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",