diff --git a/pages/chat-bubble/style-permutations.page.tsx b/pages/chat-bubble/style-permutations.page.tsx
new file mode 100644
index 0000000..7885dbf
--- /dev/null
+++ b/pages/chat-bubble/style-permutations.page.tsx
@@ -0,0 +1,129 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import Box from "@cloudscape-design/components/box";
+
+import { ChatBubble } from "../../lib/components";
+import { Page } from "../app/templates";
+import { TestBed } from "../app/test-bed";
+import { Actions, ChatBubbleAvatarGenAI, ChatBubbleAvatarUser, ChatContainer } from "./util-components";
+
+export default function ChatBubblePage() {
+ return (
+
+
+
+ {/* Background Color with Dark Mode */}
+ }
+ ariaLabel="Background test"
+ >
+ Light blue/dark purple background with adaptive text color
+
+
+ {/* Border Styles with Dark Mode */}
+ }
+ ariaLabel="Border test"
+ >
+ Adaptive red border with rounded corners
+
+
+ {/* Typography with Dark Mode */}
+ }
+ ariaLabel="Typography test"
+ >
+ Large bold adaptive purple text
+
+
+ {/* Shadow Effect with Dark Mode */}
+ }
+ ariaLabel="Shadow test"
+ >
+ Adaptive shadow for elevation
+
+
+ {/* Spacing */}
+ }
+ ariaLabel="Spacing test"
+ >
+ Wide avatar spacing with generous padding
+
+
+ {/* Loading State with Dark Mode */}
+ }
+ type="incoming"
+ showLoadingBar={true}
+ ariaLabel="Loading test"
+ >
+ Generating response...
+
+
+ {/* With Actions and Dark Mode */}
+ }
+ type="incoming"
+ actions={}
+ ariaLabel="Actions test"
+ >
+ Message with action buttons
+
+
+ {/* All Properties Combined with Dark Mode */}
+ }
+ actions={}
+ ariaLabel="All properties test"
+ showLoadingBar={true}
+ >
+ All style properties combined
+
+
+
+
+ );
+}
diff --git a/scripts/environment.js b/scripts/environment.js
index 5e832a0..ba51e8c 100644
--- a/scripts/environment.js
+++ b/scripts/environment.js
@@ -10,13 +10,17 @@ const pkg = JSON.parse(fs.readFileSync("package.json", "utf-8"));
const packageVersion = `${pkg.version} (${gitCommitVersion})`;
const basePath = "lib/components/internal/environment";
-const values = {
- PACKAGE_SOURCE: "chat-components",
- PACKAGE_VERSION: packageVersion,
- THEME: "open-source-visual-refresh",
- SYSTEM: "core",
- ALWAYS_VISUAL_REFRESH: true,
-};
+export function getBuildTimeEnvironmentConstants() {
+ return {
+ PACKAGE_SOURCE: "chat-components",
+ PACKAGE_VERSION: packageVersion,
+ THEME: "open-source-visual-refresh",
+ SYSTEM: "core",
+ ALWAYS_VISUAL_REFRESH: true,
+ };
+}
+
+const values = getBuildTimeEnvironmentConstants();
writeFile(`${basePath}.json`, JSON.stringify(values, null, 2));
writeFile(
`${basePath}.js`,
diff --git a/src/__tests__/__snapshots__/documenter.test.ts.snap b/src/__tests__/__snapshots__/documenter.test.ts.snap
index 14716bb..4e3c831 100644
--- a/src/__tests__/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/__snapshots__/documenter.test.ts.snap
@@ -215,6 +215,102 @@ If avatar is being used, set its \`loading\` state to true.",
"optional": true,
"type": "boolean",
},
+ {
+ "inlineType": {
+ "name": "ChatBubbleProps.Style",
+ "properties": [
+ {
+ "inlineType": {
+ "name": "object",
+ "properties": [
+ {
+ "name": "background",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "borderColor",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "borderRadius",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "borderWidth",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "boxShadow",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "color",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "fontSize",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "fontWeight",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "paddingBlock",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "paddingInline",
+ "optional": true,
+ "type": "string",
+ },
+ {
+ "name": "rowGap",
+ "optional": true,
+ "type": "string",
+ },
+ ],
+ "type": "object",
+ },
+ "name": "bubble",
+ "optional": true,
+ "type": "{ background?: string | undefined; borderColor?: string | undefined; borderRadius?: string | undefined; borderWidth?: string | undefined; boxShadow?: string | undefined; color?: string | undefined; ... 4 more ...; paddingInline?: string | undefined; }",
+ },
+ {
+ "inlineType": {
+ "name": "{ columnGap?: string | undefined; }",
+ "properties": [
+ {
+ "name": "columnGap",
+ "optional": true,
+ "type": "string",
+ },
+ ],
+ "type": "object",
+ },
+ "name": "root",
+ "optional": true,
+ "type": "{ columnGap?: string | undefined; }",
+ },
+ ],
+ "type": "object",
+ },
+ "name": "style",
+ "optional": true,
+ "systemTags": [
+ "core",
+ ],
+ "type": "ChatBubbleProps.Style",
+ },
{
"description": "Defines the type of the chat bubble and sets its color accordingly.",
"inlineType": {
diff --git a/src/avatar/__tests__/avatar.test.tsx b/src/avatar/__tests__/avatar.test.tsx
index 5aee133..a418a9f 100644
--- a/src/avatar/__tests__/avatar.test.tsx
+++ b/src/avatar/__tests__/avatar.test.tsx
@@ -125,6 +125,9 @@ describe("Avatar", () => {
});
test("style api", () => {
+ vi.mock("../internal/environment", () => ({
+ SYSTEM: "core",
+ }));
const ariaLabel = "User avatar JD Jane Doe";
const wrapper = renderAvatar({
ariaLabel,
diff --git a/src/chat-bubble/__tests__/__snapshots__/style.test.ts.snap b/src/chat-bubble/__tests__/__snapshots__/style.test.ts.snap
new file mode 100644
index 0000000..878b41b
--- /dev/null
+++ b/src/chat-bubble/__tests__/__snapshots__/style.test.ts.snap
@@ -0,0 +1,70 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`getBubbleStyle > handles all possible style configurations 1`] = `
+{
+ "background": undefined,
+ "borderColor": undefined,
+ "borderRadius": undefined,
+ "borderStyle": undefined,
+ "borderWidth": undefined,
+ "boxShadow": undefined,
+ "color": undefined,
+ "fontSize": undefined,
+ "fontWeight": undefined,
+ "paddingBlock": undefined,
+ "paddingInline": undefined,
+ "rowGap": undefined,
+}
+`;
+
+exports[`getBubbleStyle > handles all possible style configurations 2`] = `
+{
+ "background": undefined,
+ "borderColor": undefined,
+ "borderRadius": undefined,
+ "borderStyle": undefined,
+ "borderWidth": undefined,
+ "boxShadow": undefined,
+ "color": undefined,
+ "fontSize": undefined,
+ "fontWeight": undefined,
+ "paddingBlock": undefined,
+ "paddingInline": undefined,
+ "rowGap": undefined,
+}
+`;
+
+exports[`getBubbleStyle > handles all possible style configurations 3`] = `
+{
+ "background": "#f0f0f0",
+ "borderColor": "#ccc",
+ "borderRadius": "8px",
+ "borderStyle": "solid",
+ "borderWidth": "2px",
+ "boxShadow": "0 4px 8px rgba(0,0,0,0.2)",
+ "color": "#333",
+ "fontSize": "16px",
+ "fontWeight": "500",
+ "paddingBlock": "20px",
+ "paddingInline": "24px",
+ "rowGap": "12px",
+}
+`;
+
+exports[`getChatBubbleRootStyle > handles all possible style configurations 1`] = `
+{
+ "columnGap": undefined,
+}
+`;
+
+exports[`getChatBubbleRootStyle > handles all possible style configurations 2`] = `
+{
+ "columnGap": undefined,
+}
+`;
+
+exports[`getChatBubbleRootStyle > handles all possible style configurations 3`] = `
+{
+ "columnGap": "10px",
+}
+`;
diff --git a/src/chat-bubble/__tests__/style.test.ts b/src/chat-bubble/__tests__/style.test.ts
new file mode 100644
index 0000000..8a4a6b9
--- /dev/null
+++ b/src/chat-bubble/__tests__/style.test.ts
@@ -0,0 +1,90 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { afterEach, describe, expect, test, vi } from "vitest";
+
+import { getBubbleStyle, getChatBubbleRootStyle } from "../style";
+
+vi.mock("../internal/environment", () => ({
+ SYSTEM: "core",
+}));
+
+const allStyles = {
+ root: {
+ columnGap: "10px",
+ },
+ bubble: {
+ background: "#f0f0f0",
+ borderColor: "#ccc",
+ borderRadius: "8px",
+ borderWidth: "2px",
+ boxShadow: "0 4px 8px rgba(0,0,0,0.2)",
+ color: "#333",
+ fontSize: "16px",
+ fontWeight: "500",
+ rowGap: "12px",
+ paddingBlock: "20px",
+ paddingInline: "24px",
+ },
+};
+
+describe("getChatBubbleRootStyle", () => {
+ afterEach(() => {
+ vi.resetModules();
+ });
+
+ test("handles all possible style configurations", () => {
+ expect(getChatBubbleRootStyle(undefined)).toMatchSnapshot();
+ expect(getChatBubbleRootStyle({})).toMatchSnapshot();
+ expect(getChatBubbleRootStyle(allStyles)).toMatchSnapshot();
+ });
+
+ test("returns empty object when SYSTEM is not core", async () => {
+ vi.resetModules();
+ vi.doMock("../internal/environment", () => ({
+ SYSTEM: "visual-refresh",
+ }));
+
+ const { getChatBubbleRootStyle: getChatBubbleRootStyleNonCore } = await import("../style.js");
+
+ const style = {
+ root: {
+ columnGap: "10px",
+ },
+ };
+
+ const result = getChatBubbleRootStyleNonCore(style);
+ expect(result).toEqual({});
+ });
+});
+
+describe("getBubbleStyle", () => {
+ afterEach(() => {
+ vi.resetModules();
+ });
+
+ test("handles all possible style configurations", () => {
+ expect(getBubbleStyle(undefined)).toMatchSnapshot();
+ expect(getBubbleStyle({})).toMatchSnapshot();
+ expect(getBubbleStyle(allStyles)).toMatchSnapshot();
+ });
+
+ test("returns empty object when SYSTEM is not core", async () => {
+ vi.resetModules();
+ vi.doMock("../internal/environment", () => ({
+ SYSTEM: "visual-refresh",
+ }));
+
+ const { getBubbleStyle: getBubbleStyleNonCore } = await import("../style.js");
+
+ const style = {
+ bubble: {
+ background: "#f0f0f0",
+ borderRadius: "8px",
+ fontSize: "16px",
+ },
+ };
+
+ const result = getBubbleStyleNonCore(style);
+ expect(result).toEqual({});
+ });
+});
diff --git a/src/chat-bubble/interfaces.ts b/src/chat-bubble/interfaces.ts
index 068f48c..d564f2a 100644
--- a/src/chat-bubble/interfaces.ts
+++ b/src/chat-bubble/interfaces.ts
@@ -31,8 +31,31 @@ export interface ChatBubbleProps {
* Useful for when there are multiple consecutive messages coming from the same author.
*/
hideAvatar?: boolean;
+
+ /**
+ * @awsuiSystem core
+ */
+ style?: ChatBubbleProps.Style;
}
export namespace ChatBubbleProps {
export type Type = "incoming" | "outgoing";
+ export interface Style {
+ root?: {
+ columnGap?: string;
+ };
+ bubble?: {
+ background?: string;
+ borderColor?: string;
+ borderRadius?: string;
+ borderWidth?: string;
+ boxShadow?: string;
+ color?: string;
+ fontSize?: string;
+ fontWeight?: string;
+ rowGap?: string;
+ paddingBlock?: string;
+ paddingInline?: string;
+ };
+ }
}
diff --git a/src/chat-bubble/internal.tsx b/src/chat-bubble/internal.tsx
index bd3e798..82443c9 100644
--- a/src/chat-bubble/internal.tsx
+++ b/src/chat-bubble/internal.tsx
@@ -7,6 +7,7 @@ import { getDataAttributes } from "../internal/base-component/get-data-attribute
import { InternalBaseComponentProps } from "../internal/base-component/use-base-component";
import { InternalLoadingBar } from "../loading-bar/internal";
import { ChatBubbleProps } from "./interfaces.js";
+import { getBubbleStyle, getChatBubbleRootStyle } from "./style";
import styles from "./styles.css.js";
@@ -20,6 +21,7 @@ export default function InternalChatBubble({
showLoadingBar,
hideAvatar = false,
ariaLabel,
+ style,
__internalRootRef = null,
...rest
}: InternalChatBubbleProps) {
@@ -43,6 +45,7 @@ export default function InternalChatBubble({
ref={__internalRootRef}
role="group"
aria-label={ariaLabel}
+ style={getChatBubbleRootStyle(style)}
>
{avatar && (
@@ -50,16 +53,14 @@ export default function InternalChatBubble({
)}
-
+
{children}
-
{actions &&
{actions}
}
-
- {showLoadingBar &&
}
+ {showLoadingBar && (
+
+
+
+ )}
);
diff --git a/src/chat-bubble/style.tsx b/src/chat-bubble/style.tsx
new file mode 100644
index 0000000..b2e4139
--- /dev/null
+++ b/src/chat-bubble/style.tsx
@@ -0,0 +1,35 @@
+// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+// SPDX-License-Identifier: Apache-2.0
+import { SYSTEM } from "../internal/environment";
+import { ChatBubbleProps } from "./interfaces";
+
+export function getChatBubbleRootStyle(style: ChatBubbleProps.Style | undefined) {
+ if (SYSTEM !== "core") {
+ return {};
+ }
+
+ return {
+ columnGap: style?.root?.columnGap,
+ };
+}
+
+export function getBubbleStyle(style: ChatBubbleProps.Style | undefined) {
+ if (SYSTEM !== "core") {
+ return {};
+ }
+
+ return {
+ background: style?.bubble?.background,
+ borderColor: style?.bubble?.borderColor,
+ borderRadius: style?.bubble?.borderRadius,
+ borderStyle: style?.bubble?.borderWidth ? "solid" : undefined,
+ borderWidth: style?.bubble?.borderWidth,
+ boxShadow: style?.bubble?.boxShadow,
+ color: style?.bubble?.color,
+ fontSize: style?.bubble?.fontSize,
+ fontWeight: style?.bubble?.fontWeight,
+ paddingBlock: style?.bubble?.paddingBlock,
+ paddingInline: style?.bubble?.paddingInline,
+ rowGap: style?.bubble?.rowGap,
+ };
+}
diff --git a/src/chat-bubble/styles.scss b/src/chat-bubble/styles.scss
index cf79209..3eb0276 100644
--- a/src/chat-bubble/styles.scss
+++ b/src/chat-bubble/styles.scss
@@ -18,6 +18,7 @@
}
.message-area {
+ position: relative;
display: flex;
flex-direction: column;
gap: cs.$space-scaled-s;
@@ -30,10 +31,6 @@
border-end-start-radius: cs.$border-radius-chat-bubble;
border-end-end-radius: cs.$border-radius-chat-bubble;
- &.with-loading-bar {
- padding-block-end: 0;
- }
-
&.chat-bubble-type-outgoing {
color: cs.$color-text-chat-bubble-outgoing;
background-color: cs.$color-background-chat-bubble-outgoing;
@@ -45,6 +42,13 @@
}
}
+.loading-bar-wrapper {
+ position: absolute;
+ inset-block-end: 0;
+ inset-inline-start: 0;
+ inline-size: 100%;
+}
+
.avatar {
padding-block: cs.$space-scaled-xs;
diff --git a/vite.config.unit.mjs b/vite.config.unit.mjs
index 91e03ab..9fdc953 100644
--- a/vite.config.unit.mjs
+++ b/vite.config.unit.mjs
@@ -2,13 +2,40 @@
// SPDX-License-Identifier: Apache-2.0
import process from "node:process";
+
import { defineConfig } from "vite";
+
+import { getBuildTimeEnvironmentConstants } from "./scripts/environment.js";
import base from "./vite.config.mjs";
// https://vitejs.dev/config/
export default defineConfig({
...base,
root: "./",
+ resolve: {
+ alias: {
+ "../internal/environment": "/virtual:environment-mock",
+ },
+ },
+ plugins: [
+ ...(base.plugins || []),
+ {
+ name: "virtual-environment-mock",
+ resolveId(id) {
+ if (id === "/virtual:environment-mock") {
+ return id;
+ }
+ },
+ load(id) {
+ if (id === "/virtual:environment-mock") {
+ const values = getBuildTimeEnvironmentConstants();
+ return Object.entries(values)
+ .map(([key, value]) => `export const ${key} = ${JSON.stringify(value)};`)
+ .join("\n");
+ }
+ },
+ },
+ ],
test: {
include: ["./src/**/__tests__/**/*.test.{ts,tsx}"],
environment: "jsdom",