diff --git a/.specs/message.md b/.specs/message.md index bafa80e9e..aa592a2f0 100644 --- a/.specs/message.md +++ b/.specs/message.md @@ -4,57 +4,100 @@ category: feedback structure: monolithic status: implemented spec_version: 1 -checksum: ba56d1090963d422e4457107fa9d9de706c060de1767dd4b4efc8d9b4213dcd0 +figma: + url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=478-892 + node_id: 478:892 +checksum: 369176139640a950359df9192158948fc7e62777ee0c88b2f8fba00a1a0b3ced created: 2026-05-22 -last_updated: 2026-05-22 +last_updated: 2026-05-29 --- # Message — Component Spec ## Purpose -Communicates status, alerts, or progress to the user. Migrated from the existing implementation at `packages/webkit/src/components/webkit/feedback/message/`. +Inline feedback banner that communicates status, alerts, or progress. Presents a severity-colored surface with icon, title, optional description, and an optional text action aligned to Figma Message (478:892). + +## Usage + +```vue + + + +``` ## Props | Prop | Type | Default | Required | JSDoc | |---|---|---|---|---| -| `severity` | `'info' | 'success' | 'warning' | 'danger' | 'error'` | `'info'` | no | severity. | -| `title` | `string` | `—` | yes | title. | -| `description` | `string` | `''` | no | description. | -| `icon` | `string` | `'undefined'` | no | PrimeIcons class for the leading/trailing icon. | -| `actionLabel` | `string` | `''` | no | action Label. | +| `severity` | `'info' | 'success' | 'warning' | 'danger' | 'error'` | `'info'` | no | Visual severity variant (maps Error to danger). | +| `title` | `string` | `—` | yes | Primary message heading. | +| `description` | `string` | `''` | no | Supporting body copy below the title. | +| `icon` | `string` | `''` | no | PrimeIcons class override for the leading icon. | +| `actionLabel` | `string` | `''` | no | Label for the built-in text action button; hidden when empty. | +| `closable` | `boolean` | `false` | no | When true, shows a close control that dismisses the message. | +| `life` | `number` | `0` | no | Duration in milliseconds before auto-dismiss; `0` disables auto-dismiss. | ## Events | Event | Payload | Notes | |---|---|---| -| `action` | `unknown` | — | +| `action` | `MouseEvent` | Emitted when the built-in action button is clicked. | +| `close` | `void` | Emitted when the message is dismissed manually or after `life` expires. | ## Slots | Slot | Scope | Notes | |---|---|---| -| `action` | — | Named slot. | -| `default` | — | Main content. | +| `action` | — | Custom action control; replaces the built-in Button when provided. | +| `default` | — | Replaces the default icon + title + description layout. | ## States -- Visual states: `default`, `hover`, `focus-visible`, `active`, `disabled` +- Visual states: default (per severity) ## Motion & Animations -_none_ +| Trigger | Animation / Transition | Token | Reduced-motion fallback | +|---|---|---|---| +| dismiss | inline `opacity` transition | `duration['fast-02']` · `curve['productive-exit']` (animate.js) | `motion-reduce:transition-none` | ## Tokens | Region | Token (DESIGN.md) | |---|---| -| typography | .text-body-sm | -| surface | `var(--bg-surface)` | +| title typography | `.text-label-sm` | +| description typography | `.text-body-xs` | +| action typography | `.text-button-md` | +| surface (info) | `var(--info)` | +| surface (success) | `var(--success)` | +| surface (warning) | `var(--warning)` | +| surface (danger) | `var(--danger)` | +| border (info) | `var(--info-border)` | +| border (success) | `var(--success-border)` | +| border (warning) | `var(--warning-border)` | +| border (danger) | `var(--danger-border)` | +| icon (info) | `var(--info-contrast)` | +| icon (success) | `var(--success-contrast)` | +| icon (warning) | `var(--warning-contrast)` | +| icon (danger) | `var(--danger-contrast)` | | text | `var(--text-default)` | -| spacing | `var(--spacing-3)` | -| shape | `var(--shape-elements)` | +| muted text | `var(--text-muted)` | +| spacing (padding) | `var(--spacing-sm)` | +| spacing (gap) | `var(--spacing-xs)` | +| spacing (title stack) | `var(--spacing-xxs)` | +| shape | `var(--shape-button)` | +| shadow | `var(--shadow-xs)` | | ring | `var(--ring-color)` | +| min height | `min-h-14` | ## Theme gaps @@ -65,8 +108,8 @@ _none_ ## Accessibility (WCAG 2.1 AA) - Visible focus: `focus-visible:ring-2 focus-visible:ring-[var(--ring-color)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--bg-canvas)]` -- Keyboard map: `Tab` focuses; `Enter`/`Space` activates; `Escape` closes overlays where applicable. -- ARIA: root uses appropriate roles (`button`, `dialog`, `status`, etc.) per sub-component. +- Keyboard map: `Tab` focuses the action and close controls when present; `Enter`/`Space` activates them; `Escape` dismisses when `closable` is true. +- ARIA: root uses `role="alert"` for danger/warning severities and `role="status"` for info/success. - Contrast ≥4.5:1 (text) / ≥3:1 (large + icons), including disabled state. - `motion-reduce:transition-none motion-reduce:transform-none` on animated states. - Touch target ≥40×40 px where the control is interactive. @@ -74,6 +117,9 @@ _none_ ## Stories (Storybook) - Default +- Types +- Closable +- AutoDismiss ## Constraints — DO NOT diff --git a/apps/storybook/src/stories/primevue/Message.stories.js b/apps/storybook/src/stories/primevue/Message.stories.js index a07a1ff39..39ef636f6 100644 --- a/apps/storybook/src/stories/primevue/Message.stories.js +++ b/apps/storybook/src/stories/primevue/Message.stories.js @@ -1,4 +1,4 @@ -import Message from '@aziontech/webkit/message'; +import Message from 'primevue/message'; export default { title: 'PrimeVue/Message', diff --git a/apps/storybook/src/stories/webkit/feedback/message/Message.stories.js b/apps/storybook/src/stories/webkit/feedback/message/Message.stories.js index f9fa572c4..3c58e789f 100644 --- a/apps/storybook/src/stories/webkit/feedback/message/Message.stories.js +++ b/apps/storybook/src/stories/webkit/feedback/message/Message.stories.js @@ -1,8 +1,7 @@ -import Message from '@aziontech/webkit/feedback/message' +import Message from '@aziontech/webkit/message' -const severities = ['info', 'success', 'warning', 'danger'] - -export default { +/** @type {import('@storybook/vue3').Meta} */ +const meta = { title: 'Webkit/Feedback/Message', component: Message, tags: ['autodocs'], @@ -10,111 +9,210 @@ export default { layout: 'padded', backgrounds: { default: 'dark' + }, + a11y: { + config: { + rules: [ + { id: 'color-contrast', enabled: true }, + { id: 'focus-order-semantics', enabled: true } + ] + } + }, + docs: { + description: { + component: [ + 'Inline feedback banner that communicates status, alerts, or progress. Presents a severity-colored surface with icon, title, optional description, and an optional text action aligned to Figma Message (478:892).', + '', + '## Usage', + '', + '```vue', + '', + '', + '', + '```' + ].join('\n') + }, + source: { + type: 'dynamic', + excludeDecorators: true + }, + canvas: { + sourceState: 'shown' + } } }, argTypes: { severity: { control: 'select', - options: severities, - description: 'Visual severity variant (Figma: Info, Success, Warning, Error)' + options: ['info', 'success', 'warning', 'danger', 'error'], + description: 'Visual severity variant (maps Error to danger).', + table: { + category: 'props', + type: { summary: "'info' | 'success' | 'warning' | 'danger' | 'error'" }, + defaultValue: { summary: "'info'" } + } }, title: { control: 'text', - description: 'Message title' + description: 'Primary message heading.', + table: { category: 'props', type: { summary: 'string', required: true } } }, description: { control: 'text', - description: 'Supporting description text' + description: 'Supporting body copy below the title.', + table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } } }, icon: { control: 'text', - description: 'PrimeIcons class override' + description: 'PrimeIcons class override for the leading icon.', + table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } } }, actionLabel: { control: 'text', - description: 'Action button label (hidden when empty)' + description: 'Label for the built-in text action button; hidden when empty.', + table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } } + }, + closable: { + control: 'boolean', + description: 'When true, shows a close control that dismisses the message.', + table: { category: 'props', type: { summary: 'boolean' }, defaultValue: { summary: 'false' } } + }, + life: { + control: 'number', + description: 'Duration in milliseconds before auto-dismiss; `0` disables auto-dismiss.', + table: { category: 'props', type: { summary: 'number' }, defaultValue: { summary: '0' } } + }, + onAction: { + action: 'action', + description: 'Emitted when the built-in action button is clicked.', + table: { category: 'events', type: { summary: 'MouseEvent' } } + }, + onClose: { + action: 'close', + description: 'Emitted when the message is dismissed manually or after `life` expires.', + table: { category: 'events', type: { summary: 'void' } } } - } -} - -export const Default = { + }, args: { severity: 'info', title: 'Info message', description: 'A brief description of the message.', - actionLabel: 'Label' - }, - render: (args) => ({ - components: { Message }, - setup() { - return { args } - }, - template: ` - - ` + actionLabel: 'Label', + icon: '', + closable: false, + life: 0 + } +} + +export default meta + +const messageRemountKey = (args) => + JSON.stringify({ + severity: args.severity, + title: args.title, + description: args.description, + icon: args.icon, + actionLabel: args.actionLabel, + closable: args.closable, + life: args.life }) + +const Template = (args) => ({ + components: { Message }, + setup() { + const { onAction, onClose, ...props } = args + + return { props, onAction, onClose, remountKey: messageRemountKey(args) } + }, + template: '' +}) + +/** @type {import('@storybook/vue3').StoryObj} */ +export const Default = { + render: Template, + parameters: { + docs: { description: { story: 'Default info message with title, description, and action.' } } + } } -export const Severities = { +/** @type {import('@storybook/vue3').StoryObj} */ +export const Types = { render: () => ({ components: { Message }, template: ` -
+
+ + +
` - }) + }), + parameters: { + docs: { + controls: { disable: true }, + description: { story: 'All severity variants stacked.' } + } + } } -export const WithoutAction = { +/** @type {import('@storybook/vue3').StoryObj} */ +export const Closable = { args: { - severity: 'info', - title: 'Info message', - description: 'A brief description of the message.', - actionLabel: '' + closable: true }, - render: (args) => ({ - components: { Message }, - setup() { - return { args } - }, - template: ` - - ` - }) + render: Template, + parameters: { + docs: { + description: { story: 'Closable message with a dismiss control on the trailing edge.' } + } + } } -export const CustomAction = { - render: () => ({ - components: { Message }, - template: ` - - - - ` - }) +/** @type {import('@storybook/vue3').StoryObj} */ +export const AutoDismiss = { + args: { + life: 5000, + actionLabel: '' + }, + render: Template, + parameters: { + docs: { + description: { + story: 'Auto-dismisses after 5 seconds when `life` is greater than zero.' + } + } + } } diff --git a/packages/webkit/package.json b/packages/webkit/package.json index cf751c635..fd3965e17 100644 --- a/packages/webkit/package.json +++ b/packages/webkit/package.json @@ -35,7 +35,8 @@ "primevue": "3.35.0", "tailwind-merge": "^3.6.0", "vee-validate": "^4.15.1", - "vue-tsc": "^3.2.5" + "vue-tsc": "^3.2.5", + "@aziontech/theme": "^2.2.1" }, "devDependencies": { "@figma/code-connect": "^1.4.5", @@ -112,7 +113,6 @@ "./divider": "./src/core/primevue/divider/divider.vue", "./inlinemessage": "./src/core/primevue/inline-message/inline-message.vue", "./menu": "./src/core/primevue/menu/menu.vue", - "./message": "./src/core/primevue/message/message.vue", "./overlaypanel": "./src/core/primevue/overlaypanel/overlaypanel.vue", "./paginator": "./src/core/primevue/paginator/paginator.vue", "./progressbar": "./src/core/primevue/progressbar/progressbar.vue", @@ -167,7 +167,7 @@ "./data/data-table-toolbar": "./src/components/data/data-table/data-table-toolbar.vue", "./data/data-table-view-all-footer": "./src/components/data/data-table/data-table-view-all-footer.vue", "./data/data-table-view-toggle": "./src/components/data/data-table/data-table-view-toggle.vue", - "./feedback/message": "./src/components/feedback/message/message.vue", + "./message": "./src/components/feedback/message/message.vue", "./feedback/status-indicator": "./src/components/feedback/status-indicator/status-indicator.vue", "./inputs/input-text": "./src/components/inputs/input-text/input-text.vue", "./inputs/box-grid-selection": "./src/components/inputs/box-grid-selection/box-grid-selection.vue", diff --git a/packages/webkit/src/components/feedback/message/message.vue b/packages/webkit/src/components/feedback/message/message.vue index 603cdfa4b..673583c3d 100644 --- a/packages/webkit/src/components/feedback/message/message.vue +++ b/packages/webkit/src/components/feedback/message/message.vue @@ -1,154 +1,211 @@ - diff --git a/packages/webkit/src/components/feedback/message/presets/transitions.ts b/packages/webkit/src/components/feedback/message/presets/transitions.ts new file mode 100644 index 000000000..adc6b7de8 --- /dev/null +++ b/packages/webkit/src/components/feedback/message/presets/transitions.ts @@ -0,0 +1,20 @@ +import { curve, duration } from '@aziontech/theme/animations' + +/** + * Message dismiss motion — values read only from `animate.js` (`duration`, `curve`). + * Applied via inline `transition` (Tailwind does not emit dynamic `duration-[…]` classes). + */ +export const messageDismissMotion = { + duration: duration['fast-02'], + curve: curve['productive-exit'] +} as const + +/** Defers unmount until the opacity exit finishes. */ +export const MESSAGE_DISMISS_MS = Number.parseInt(messageDismissMotion.duration, 10) + +/** Inline transition for dismiss (opacity fade-out). */ +export const getMessageDismissTransitionStyle = (): { transition: string } => ({ + transition: `opacity ${messageDismissMotion.duration} ${messageDismissMotion.curve}` +}) + +export const messageDismissTransitionClasses = ['motion-reduce:transition-none'] as const diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 6690e2392..773b8ab4c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -2,6 +2,14 @@ packages: - "apps/*" - "packages/*" +allowBuilds: + '@parcel/watcher': set this to true or false + es5-ext: set this to true or false + esbuild: set this to true or false + ttf2woff2: set this to true or false + unrs-resolver: set this to true or false + vue-demi: set this to true or false + overrides: handlebars: ">=4.7.9" tar: ">=7.5.11"