From 464a6740a3eff9b85655769cb8ef68aee073feeb Mon Sep 17 00:00:00 2001 From: TarunAdobe Date: Wed, 18 Mar 2026 14:58:38 +0530 Subject: [PATCH 01/33] feat: add new conversational-ai pattern --- 2nd-gen/packages/swc/.storybook/main.ts | 10 + 2nd-gen/packages/swc/.storybook/preview.ts | 2 + 2nd-gen/packages/swc/cem.config.js | 1 + .../swc/patterns/conversational-ai/README.mdx | 33 ++ .../prompt-field/PromptField.ts | 203 ++++++++++++ .../conversational-ai/prompt-field/index.ts | 24 ++ .../prompt-field/prompt-field.css | 276 +++++++++++++++++ .../stories/prompt-field.stories.ts | 291 ++++++++++++++++++ .../test/prompt-field.a11y.spec.ts | 43 +++ .../prompt-field/test/prompt-field.test.ts | 167 ++++++++++ .../user-message/UserMessage.ts | 56 ++++ .../conversational-ai/user-message/index.ts | 24 ++ .../stories/user-message.stories.ts | 276 +++++++++++++++++ .../test/user-message.a11y.spec.ts | 39 +++ .../user-message/test/user-message.test.ts | 92 ++++++ .../user-message/user-message.css | 48 +++ 16 files changed, 1585 insertions(+) create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/README.mdx create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/index.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/stories/prompt-field.stories.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.a11y.spec.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/test/prompt-field.test.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/UserMessage.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/index.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/stories/user-message.stories.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/test/user-message.a11y.spec.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/test/user-message.test.ts create mode 100644 2nd-gen/packages/swc/patterns/conversational-ai/user-message/user-message.css diff --git a/2nd-gen/packages/swc/.storybook/main.ts b/2nd-gen/packages/swc/.storybook/main.ts index 12e4fbf7ea1..8868e0c795a 100644 --- a/2nd-gen/packages/swc/.storybook/main.ts +++ b/2nd-gen/packages/swc/.storybook/main.ts @@ -50,6 +50,16 @@ const stories: StorybookConfig['stories'] = [ : '**/*.stories.ts', titlePrefix: 'Components', }, + { + directory: '../patterns', + files: '**/*.stories.ts', + titlePrefix: 'Patterns', + }, + { + directory: '../patterns', + files: '**/*.mdx', + titlePrefix: 'Patterns', + }, ]; /** diff --git a/2nd-gen/packages/swc/.storybook/preview.ts b/2nd-gen/packages/swc/.storybook/preview.ts index 16138ca57cd..825825e799f 100644 --- a/2nd-gen/packages/swc/.storybook/preview.ts +++ b/2nd-gen/packages/swc/.storybook/preview.ts @@ -216,6 +216,8 @@ const preview = { 'Learn about SWC', ['Overview', 'When to use SWC', '1st-gen vs 2nd-gen'], 'Components', + 'Patterns', + ['Conversational AI', ['README', 'Prompt field', 'User message']], 'Guides', [ 'Accessibility guides', diff --git a/2nd-gen/packages/swc/cem.config.js b/2nd-gen/packages/swc/cem.config.js index 4d5a2c5fe78..2c489fa00b6 100644 --- a/2nd-gen/packages/swc/cem.config.js +++ b/2nd-gen/packages/swc/cem.config.js @@ -13,6 +13,7 @@ export default { globs: [ 'components/**/*.ts', + 'patterns/**/*.ts', '../core/components/**/*.ts', '../core/controllers/**/*.ts', '../core/element/**/*.ts', diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/README.mdx b/2nd-gen/packages/swc/patterns/conversational-ai/README.mdx new file mode 100644 index 00000000000..7cb51f542da --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/README.mdx @@ -0,0 +1,33 @@ +import { Meta } from '@storybook/addon-docs/blocks'; + + + +# Conversational AI + +Starter pattern area for conversational AI building blocks and composition in 2nd-gen. + +## Included building blocks + +- `prompt-field`: input surface for entering or reviewing a prompt +- `user-message`: message bubble for user-authored content in a conversation thread + +## Current scope + +This is intentionally minimal. The goal right now is to establish the folder structure, Storybook visibility, and token-based styling approach before the pattern grows into a fuller API. + +## Pattern structure + +```text +patterns/ + conversational-ai/ + README.mdx + conversational-ai.stories.ts + prompt-field/ + user-message/ +``` + +## Next steps + +- add richer prompt-field states like disabled, loading, and multi-line growth +- add additional conversation roles such as assistant/system responses +- decide whether pattern-level exports should ship from the package build diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts new file mode 100644 index 00000000000..320e8747b17 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/PromptField.ts @@ -0,0 +1,203 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { CSSResultArray, html, TemplateResult } from 'lit'; +import { property } from 'lit/decorators.js'; + +import { SpectrumElement } from '@spectrum-web-components/core/element/index.js'; + +import '@adobe/spectrum-wc/icon'; + +import { + AlertIcon, + Arrow100Icon, + Asterisk100Icon, + Cross100Icon, + Dash100Icon, +} from '../../../components/icon/elements/index.js'; + +import styles from './prompt-field.css'; + +/** + * Prompt entry surface for conversational AI flows. + * + * Fires events for all interactions; consumers are responsible for managing state. + * + * @element swc-prompt-field + */ +export class PromptField extends SpectrumElement { + /** Controls whether the send button or stop button is shown on the right. */ + @property({ type: String, reflect: true }) + public state: 'default' | 'send' | 'stop' = 'default'; + + /** + * Whether an uploaded artifact is shown above the text input. + * `none` hides the artifact area. + */ + @property({ type: String, reflect: true, attribute: 'uploaded-artifact' }) + public uploadedArtifact: 'none' | 'card' | 'image' = 'none'; + + /** When `true`, the send button is enabled. Set this based on whether the textarea has content. */ + @property({ type: Boolean, reflect: true }) + public populated = false; + + /** Accessible label shown above the textarea. */ + @property({ type: String }) + public label = 'Prompt'; + + /** Placeholder text shown inside the textarea. */ + @property({ type: String }) + public placeholder = 'Ask anything'; + + /** The current textarea value. Controlled by the consumer. */ + @property({ type: String }) + public value = ''; + + public static override get styles(): CSSResultArray { + return [styles]; + } + + private _handleInput(event: Event): void { + const textarea = event.target as HTMLTextAreaElement; + this.value = textarea.value; + this.dispatchEvent( + new CustomEvent('swc-input', { + bubbles: true, + composed: true, + detail: { value: this.value }, + }) + ); + } + + private _handleSendClick(): void { + if (!this.populated) { + return; + } + this.dispatchEvent( + new CustomEvent('swc-submit', { bubbles: true, composed: true }) + ); + } + + private _handleStopClick(): void { + this.dispatchEvent( + new CustomEvent('swc-stop', { bubbles: true, composed: true }) + ); + } + + private _handleUploadClick(): void { + this.dispatchEvent( + new CustomEvent('swc-upload-click', { bubbles: true, composed: true }) + ); + } + + private _handleArtifactDismiss(): void { + this.dispatchEvent( + new CustomEvent('swc-artifact-dismiss', { bubbles: true, composed: true }) + ); + } + + private _renderArtifact(): TemplateResult { + return html` +
+ + +
+ `; + } + + private _renderSendButton(): TemplateResult { + return html` + + `; + } + + private _renderStopButton(): TemplateResult { + return html` + + `; + } + + protected override render(): TemplateResult { + const showArtifact = this.uploadedArtifact !== 'none'; + const showStop = this.state === 'stop'; + + return html` +
+
+
+ ${showArtifact ? this._renderArtifact() : ''} +
+ ${this.label} + +
+
+ +
+ + + ${showStop ? this._renderStopButton() : this._renderSendButton()} +
+
+ +

+ Verify responses. + + Adobe Generative AI User Guidelines + + +

+
+ `; + } +} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/index.ts b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/index.ts new file mode 100644 index 00000000000..74b3077e83e --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/index.ts @@ -0,0 +1,24 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { defineElement } from '@spectrum-web-components/core/element/index.js'; + +import { PromptField } from './PromptField.js'; + +export * from './PromptField.js'; + +declare global { + interface HTMLElementTagNameMap { + 'swc-prompt-field': PromptField; + } +} + +defineElement('swc-prompt-field', PromptField); diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css new file mode 100644 index 00000000000..d5204705458 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/prompt-field.css @@ -0,0 +1,276 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +:host { + display: block; + inline-size: 100%; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +/* ───────────────────────────────────── + Outer wrapper + ───────────────────────────────────── */ + +.swc-PromptField { + display: flex; + flex-direction: column; + gap: 12px; + inline-size: 100%; +} + +/* ───────────────────────────────────── + White card box with shadow + ───────────────────────────────────── */ + +.swc-PromptField-box { + display: flex; + flex-direction: column; + gap: 0; + padding: 16px; + background: token("gray-25"); + border-radius: 16px; + box-shadow: token("drop-shadow-emphasized-default-x") token("drop-shadow-emphasized-default-y") token("drop-shadow-emphasized-default-blur") token("drop-shadow-emphasized-default-color"); + overflow: hidden; +} + +/* ───────────────────────────────────── + Input area (artifact + text area) + ───────────────────────────────────── */ + +.swc-PromptField-input-area { + display: flex; + flex-direction: column; + gap: 16px; + padding-block-end: 4px; +} + +/* ───────────────────────────────────── + Uploaded artifact + ───────────────────────────────────── */ + +.swc-PromptField-artifact { + position: relative; + block-size: 68px; + border-radius: token("corner-radius-medium-size-medium"); + overflow: hidden; +} + +:host([uploaded-artifact="image"]) .swc-PromptField-artifact { + inline-size: 68px; +} + +.swc-PromptField-artifact-dismiss { + display: flex; + position: absolute; + inset-block-start: 4px; + inset-inline-end: 4px; + align-items: center; + justify-content: center; + inline-size: 24px; + block-size: 24px; + padding: 0; + color: token("gray-25"); + background: token("gray-900"); + border: none; + border-radius: 50%; + cursor: pointer; +} + +.swc-PromptField-artifact-dismiss:hover { + background: token("gray-800"); +} + +.swc-PromptField-artifact-dismiss swc-icon { + --swc-icon-size: 10px; +} + +/* ───────────────────────────────────── + Text area (label + textarea) + ───────────────────────────────────── */ + +.swc-PromptField-text-area { + display: flex; + flex-direction: column; + gap: 4px; +} + +.swc-PromptField-label { + display: block; + font-family: token("sans-serif-font"); + font-size: token("font-size-100"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-100"); + color: token("gray-700"); +} + +.swc-PromptField-textarea { + field-sizing: content; + display: block; + inline-size: 100%; + min-block-size: calc(token("font-size-100") * token("line-height-100")); + padding: 0; + font-family: token("sans-serif-font"); + font-size: token("font-size-100"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-100"); + color: token("gray-800"); + background: transparent; + border: none; + resize: none; + outline: none; +} + +.swc-PromptField-textarea::placeholder { + color: token("gray-500"); +} + +/* ───────────────────────────────────── + Action bar + ───────────────────────────────────── */ + +.swc-PromptField-action-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding-block-start: 12px; +} + +/* Upload button (quiet, left side) */ + +.swc-PromptField-upload { + display: flex; + align-items: center; + justify-content: center; + inline-size: 32px; + block-size: 32px; + padding: 0; + color: token("gray-700"); + background: transparent; + border: none; + border-radius: token("corner-radius-medium-size-medium"); + cursor: pointer; +} + +.swc-PromptField-upload:hover { + color: token("gray-800"); + background: token("gray-75"); +} + +.swc-PromptField-upload swc-icon { + --swc-icon-size: 18px; +} + +/* Send button (filled circle, right side) */ + +.swc-PromptField-send { + display: flex; + align-items: center; + justify-content: center; + inline-size: 32px; + block-size: 32px; + padding: 0; + color: token("gray-25"); + background: token("accent-background-color-default"); + border: none; + border-radius: 50%; + cursor: pointer; + transition: background 130ms ease; +} + +.swc-PromptField-send:disabled { + color: token("gray-500"); + background: token("gray-200"); + cursor: default; +} + +.swc-PromptField-send:hover:not(:disabled) { + background: token("accent-background-color-hover"); +} + +.swc-PromptField-send swc-icon { + --swc-icon-size: 14px; +} + +/* Stop button (filled black circle, right side) */ + +.swc-PromptField-stop { + display: flex; + align-items: center; + justify-content: center; + inline-size: 32px; + block-size: 32px; + padding: 0; + color: token("gray-25"); + background: token("gray-900"); + border: none; + border-radius: 50%; + cursor: pointer; + transition: background 130ms ease; +} + +.swc-PromptField-stop:hover { + background: token("gray-800"); +} + +.swc-PromptField-stop swc-icon { + --swc-icon-size: 14px; +} + +/* ───────────────────────────────────── + Legal disclaimer + ───────────────────────────────────── */ + +.swc-PromptField-disclaimer { + display: flex; + gap: 4px; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + font-family: token("sans-serif-font"); + font-size: token("font-size-75"); + font-weight: token("regular-font-weight"); + line-height: token("line-height-font-size-75"); + color: token("gray-700"); + text-align: center; +} + +.swc-PromptField-disclaimer-link { + color: token("gray-700"); + text-decoration: underline; + text-underline-offset: 2px; +} + +.swc-PromptField-disclaimer-link:hover { + color: token("gray-800"); +} + +.swc-PromptField-disclaimer-info { + display: inline-flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + padding: 0; + color: token("gray-700"); + background: transparent; + border: none; + cursor: pointer; +} + +.swc-PromptField-disclaimer-info:hover { + color: token("gray-800"); +} diff --git a/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/stories/prompt-field.stories.ts b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/stories/prompt-field.stories.ts new file mode 100644 index 00000000000..0518eb010f2 --- /dev/null +++ b/2nd-gen/packages/swc/patterns/conversational-ai/prompt-field/stories/prompt-field.stories.ts @@ -0,0 +1,291 @@ +/** + * Copyright 2026 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +import { html } from 'lit'; +import type { Meta, StoryObj as Story } from '@storybook/web-components'; +import { getStorybookHelpers } from '@wc-toolkit/storybook-helpers'; + +import '../index.js'; + +// ──────────────── +// METADATA +// ──────────────── + +const { args, argTypes, template } = getStorybookHelpers('swc-prompt-field'); + +argTypes.state = { + ...argTypes.state, + control: { type: 'select' }, + options: ['default', 'send', 'stop'], + table: { + category: 'attributes', + defaultValue: { summary: 'default' }, + }, +}; + +argTypes['uploaded-artifact'] = { + ...argTypes['uploaded-artifact'], + control: { type: 'select' }, + options: ['none', 'card', 'image'], + table: { + category: 'attributes', + defaultValue: { summary: 'none' }, + }, +}; + +/** + * The prompt entry surface for conversational AI flows. + * Fires events for all interactions — consumers manage state externally. + */ +const meta: Meta = { + title: 'Conversational AI/Prompt field', + component: 'swc-prompt-field', + args, + argTypes, + render: (args) => template(args), + parameters: { + docs: { + subtitle: 'Prompt entry surface for conversational AI flows.', + }, + layout: 'padded', + }, + excludeStories: ['meta'], +}; + +export default meta; + +// ──────────────────── +// AUTODOCS STORY +// ──────────────────── + +export const Playground: Story = { + args: { + label: 'Prompt', + placeholder: 'Ask anything', + value: '', + state: 'default', + 'uploaded-artifact': 'none', + populated: false, + }, + tags: ['autodocs', 'dev'], +}; + +// ────────────────────────────── +// OVERVIEW STORY +// ────────────────────────────── + +export const Overview: Story = { + args: { + label: 'Prompt', + placeholder: 'Ask anything', + value: '', + state: 'default', + 'uploaded-artifact': 'none', + populated: false, + }, + tags: ['overview'], +}; + +// ────────────────────────── +// ANATOMY STORY +// ────────────────────────── + +/** + * A prompt field consists of: + * + * 1. **Box** — White card with shadow and 16px border radius + * 2. **Input area** — Optional artifact preview + prompt label + textarea + * 3. **Action bar** — Upload button (left) and send/stop button (right) + * 4. **Disclaimer** — Legal attribution below the card + */ +export const Anatomy: Story = { + render: () => html` + + `, + tags: ['anatomy'], +}; + +// ────────────────────────── +// OPTIONS STORIES +// ────────────────────────── + +/** + * The `state` attribute controls which action button appears on the right side of the action bar: + * + * - **`default`** — Send button is shown but disabled (no content yet) + * - **`send`** — Send button is enabled (content present); set via `populated` attribute + * - **`stop`** — Stop button is shown while the AI is generating a response + */ +export const State: Story = { + render: () => html` +
+
+ + + Default — send disabled + +
+
+ + + Send — populated, send enabled + +
+
+ + + Stop — AI is generating + +
+
+ `, + parameters: { 'section-order': 1 }, + tags: ['options'], +}; + +/** + * The `uploaded-artifact` attribute controls whether an artifact preview appears above the text input: + * + * - **`none`** — No artifact (default) + * - **`card`** — Horizontal card attachment (e.g. a linked file or project) + * - **`image`** — Square image/asset thumbnail + * + * Slot the artifact content into the `artifact` named slot. + * The component always renders the dismiss button over the artifact. + */ +export const UploadedArtifact: Story = { + render: () => html` +
+
+ + + None + +
+
+ +
+
+
+
+ Hilton commercial assets +
+
+ 2026 +
+
+
+
+ + Card + +
+
+ +
+
+
+
+ + Image + +
+
+ `, + parameters: { 'section-order': 2 }, + tags: ['options'], +}; + +// ──────────────────────────────── +// ACCESSIBILITY STORY +// ──────────────────────────────── + +/** + * ### Features + * + * The `` element implements the following accessibility features: + * + * #### Label association + * + * - The `