diff --git a/.specs/message.md b/.specs/message.md index a684c7d5f..2cb7a5cd2 100644 --- a/.specs/message.md +++ b/.specs/message.md @@ -7,15 +7,15 @@ spec_version: 1 figma: url: https://www.figma.com/design/t97pXRs7xME3SJDs5iZ5RF/Webkit?node-id=478-892 node_id: 478:892 -checksum: 369176139640a950359df9192158948fc7e62777ee0c88b2f8fba00a1a0b3ced +checksum: 0d839fb3512e4e880b75bdbcc58d286e2304a4ab23ef3f968c3d1a621647d696 created: 2026-05-22 -last_updated: 2026-05-29 +last_updated: 2026-07-29 --- # Message — Component Spec ## Purpose -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). +Inline feedback banner that communicates status, alerts, or progress. Presents a severity-colored surface with a leading icon and a single line of message copy that may carry inline links, plus an optional secondary action button. ## Usage @@ -25,12 +25,18 @@ import Message from '@aziontech/webkit/message' ``` @@ -39,10 +45,10 @@ import Message from '@aziontech/webkit/message' | Prop | Type | Default | Required | JSDoc | |---|---|---|---|---| | `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. | +| `size` | `'small' | 'medium'` | `'medium'` | no | Size token. Drives the banner height, inline padding, copy scale, and the trailing control sizes. | +| `label` | `string` | `''` | no | Fallback message copy when the default slot is empty. | | `icon` | `string` | `''` | no | PrimeIcons class override for the leading icon. | -| `actionLabel` | `string` | `''` | no | Label for the built-in text action button; hidden when empty. | +| `actionLabel` | `string` | `''` | no | Label for the built-in secondary 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. | @@ -58,7 +64,7 @@ import Message from '@aziontech/webkit/message' | Slot | Scope | Notes | |---|---|---| | `action` | — | Custom action control; replaces the built-in Button when provided. | -| `default` | — | Replaces the default icon + title + description layout. | +| `default` | — | Message copy. Accepts inline content — plain text plus anchors, which the message region styles with the `.text-link` token. Falls back to `label` when empty. | ## States @@ -74,9 +80,18 @@ import Message from '@aziontech/webkit/message' | Region | Token (DESIGN.md) | |---|---| -| title typography | `.text-label-sm` | -| description typography | `.text-body-xs` | -| action typography | `.text-button-md` | +| message typography (small) | `.text-label-sm` — 12px | +| message typography (medium) | `.text-label-md` — 14px | +| inline link (anchor inside the message) | `.text-link` | +| leading icon glyph | `.text-label-md` — 14px at both sizes; the icon does not scale with the banner | +| leading icon box | `size-3.5` — 14px, matching the glyph so the row height is stable | +| action typography | `.text-button-md` — both Button sizes use this step | +| action size | tracks `size`: `small` → Button `small` (28px), `medium` → Button `medium` (32px) | +| action surface | `var(--secondary)` — the built-in action is a `secondary` Button, so it reads as a control against every severity surface rather than as body copy | +| action text | `var(--secondary-contrast)` | +| close control size | tracks `size`: `small` → IconButton `small` (28px), `medium` → IconButton `medium` (32px) | +| close control surface | transparent — the close is a `transparent` IconButton, so the severity surface shows through and the glyph does not compete with the action | +| close control glyph | `var(--text-default)` | | surface (info) | `var(--info)` | | surface (success) | `var(--success)` | | surface (warning) | `var(--warning)` | @@ -91,25 +106,30 @@ import Message from '@aziontech/webkit/message' | icon (danger) | `var(--danger-contrast)` | | text | `var(--text-default)` | | 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)` | +| spacing (padding block) | `py-1.5` — 6px; no semantic token sits between `--spacing-xxs` (4px) and `--spacing-xs` (8px) | +| spacing (padding inline, small) | `var(--spacing-xs)` — 8px | +| spacing (padding inline, medium) | `var(--spacing-sm)` — 12px | +| spacing (padding inline end, with a trailing control) | `var(--spacing-xs)` — 8px, tightened; the trailing action/close control carries its own padding | +| spacing (gap) | `var(--spacing-sm)` — 12px at every breakpoint; only the `lg`/`xl`/`xxl` steps scale responsively | +| shape | `var(--shape-button)` — 6px, identical to `rounded-md` | | shadow | `var(--shadow-xs)` | | ring | `var(--ring-color)` | -| min height | `min-h-14` | +| height (small) | `min-h-8` — 32px floor; grows with wrapped copy or a trailing control | +| height (medium) | `min-h-9` — 36px floor; grows with wrapped copy or a trailing control | ## Theme gaps | Figma variable | Temporary primitive | Follow-up | |---|---|---| -| _none_ | — | — | +| block padding 6px | `py-1.5` (`calc(var(--spacing) * 1.5)`) | Add a 6px semantic spacing step if a second component needs it; today only this banner does. | +| icon box 14px | `size-3.5` (`calc(var(--spacing) * 3.5)`) | The theme has no icon-size scale, so the box is spacing-derived while the glyph rides the `.text-label-md` font-size token. Add an icon-size scale when a second component needs a non-16px glyph. | ## 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 the action and close controls when present; `Enter`/`Space` activates them; `Escape` dismisses when `closable` is true. +- Keyboard map: `Tab` focuses inline links in the message copy, then the action and close controls when present; `Enter` follows a link, `Enter`/`Space` activates the controls; `Escape` dismisses when `closable` is true. - ARIA: root uses `role="alert"` for danger/warning severities and `role="status"` for info/success. +- Inline links are underlined, not colour-only, so they stay distinguishable against every severity surface (WCAG 1.4.1); the `.text-link` token supplies the focus outline. - 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. @@ -118,6 +138,8 @@ import Message from '@aziontech/webkit/message' - Default - Types +- Sizes +- WithLinks — the inline-link anatomy is the reason this component was reshaped; the default slot's link handling is not reachable from any arg-driven story, so it needs its own composite story. - Closable - AutoDismiss diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-mobile.png index 74b7dbeb7..fc2a15953 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-tablet.png index 8130a415a..ed2a31b15 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-desktop.png index 87cb09a23..53f7e7b09 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-desktop.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-mobile.png index 5fc906823..61d89edb6 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-tablet.png index 040f46f3b..96d944c0e 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss.png index f4eaaf621..2a478fcbc 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--auto-dismiss.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-mobile.png index 7c445888b..295d0d356 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-tablet.png index 281fe8bc6..49df25121 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-desktop.png index 145dece54..b0e3758a3 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-desktop.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-mobile.png index d6c106b6f..add09f7b8 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-tablet.png index 1b3638880..244ff1909 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable.png index 1b23840ba..879209e03 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--closable.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-mobile.png index a2adc1c64..4098a9419 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-tablet.png index ea6dbb228..9c7111d42 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-desktop.png index 064ccddae..55a86df2c 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-desktop.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-mobile.png index e11034f34..c514937b4 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-tablet.png index 943551f18..0d1ec825b 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default.png index db0911d15..431b0efa0 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--default.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-mobile.png new file mode 100644 index 000000000..ac55b3267 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-tablet.png new file mode 100644 index 000000000..cd4669512 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-desktop.png new file mode 100644 index 000000000..b3f074310 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-mobile.png new file mode 100644 index 000000000..1519cb3e0 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-tablet.png new file mode 100644 index 000000000..b141da6b8 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes.png new file mode 100644 index 000000000..872f840ae Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--sizes.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-mobile.png index 6a0bfe8af..15c3c36f0 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-tablet.png index 1167bd69e..9489328d3 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-desktop.png index e12b6f94b..57b0b11f4 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-desktop.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-mobile.png index 30a990982..c0b2d1a4d 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-mobile.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-tablet.png index 7b32f8920..59f15ec20 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-tablet.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types.png index 7c0b2174c..b94865834 100644 Binary files a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types.png and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--types.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-mobile.png new file mode 100644 index 000000000..55c0ad6ff Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-tablet.png new file mode 100644 index 000000000..e0bc8922a Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--dark-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-desktop.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-desktop.png new file mode 100644 index 000000000..3ba92bb91 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-desktop.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-mobile.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-mobile.png new file mode 100644 index 000000000..fad5bf547 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-mobile.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-tablet.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-tablet.png new file mode 100644 index 000000000..c74c965c6 Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links--light-tablet.png differ diff --git a/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links.png b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links.png new file mode 100644 index 000000000..f13c8ae0e Binary files /dev/null and b/apps/storybook/.storybook/test-visual/__image_snapshots__/linux/components-feedback-message--with-links.png differ diff --git a/apps/storybook/src/stories/components/feedback/message/Message.stories.js b/apps/storybook/src/stories/components/feedback/message/Message.stories.js index 02a5b53fb..bcd627271 100644 --- a/apps/storybook/src/stories/components/feedback/message/Message.stories.js +++ b/apps/storybook/src/stories/components/feedback/message/Message.stories.js @@ -26,7 +26,7 @@ const meta = { 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.' + 'Inline feedback banner that communicates status, alerts, or progress. Presents a severity-colored surface with a leading icon and a single line of message copy that may carry inline links, plus an optional secondary action button.' }, canvas: { sourceState: 'shown' } } @@ -42,14 +42,20 @@ const meta = { defaultValue: { summary: "'info'" } } }, - title: { - control: 'text', - description: 'Primary message heading.', - table: { category: 'props', type: { summary: 'string', required: true } } + size: { + control: 'inline-radio', + options: ['small', 'medium'], + description: + 'Size token. Drives the banner height, inline padding, copy scale, and the trailing control sizes.', + table: { + category: 'props', + type: { summary: "'small' | 'medium'" }, + defaultValue: { summary: "'medium'" } + } }, - description: { + label: { control: 'text', - description: 'Supporting body copy below the title.', + description: 'Fallback message copy when the default slot is empty.', table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } } }, icon: { @@ -59,7 +65,7 @@ const meta = { }, actionLabel: { control: 'text', - description: 'Label for the built-in text action button; hidden when empty.', + description: 'Label for the built-in secondary action button; hidden when empty.', table: { category: 'props', type: { summary: 'string' }, defaultValue: { summary: "''" } } }, closable: { @@ -84,20 +90,21 @@ const meta = { }, default: { control: false, - description: 'Replaces the default icon + title + description layout.', + description: + 'Message copy. Accepts inline content — plain text plus anchors, which the message region styles with the `.text-link` token. Falls back to `label` when empty.', table: { category: 'slots' } }, action: { control: false, - description: 'Custom action control; replaces the built-in text action button when provided.', + description: 'Custom action control; replaces the built-in Button when provided.', table: { category: 'slots' } } }, args: { severity: 'info', - title: 'Info message', - description: 'A brief description of the message.', - actionLabel: 'Label', + size: 'medium', + label: 'Your workload finished deploying in 42 seconds.', + actionLabel: 'Action', icon: '', closable: false, life: 0 @@ -114,8 +121,8 @@ const Template = (args) => ({ const remountKey = computed(() => JSON.stringify({ severity: args.severity, - title: args.title, - description: args.description, + size: args.size, + label: args.label, icon: args.icon, actionLabel: args.actionLabel, closable: args.closable, @@ -129,8 +136,7 @@ const Template = (args) => ({ const DEFAULT_MARKUP = `` @@ -139,7 +145,7 @@ export const Default = { render: Template, parameters: { docs: { - description: { story: 'Default info message with title, description, and action.' }, + description: { story: 'Default info message: leading icon, one line of copy, and an action.' }, source: { code: toSfc(IMPORT, DEFAULT_MARKUP) } } } @@ -148,26 +154,22 @@ export const Default = { const TYPES_TEMPLATE = `
` @@ -184,10 +186,66 @@ export const Types = { } } +const SIZES_TEMPLATE = `
+ + +
` + +/** @type {import('@storybook/vue3').StoryObj} */ +export const Sizes = { + render: () => ({ components: { Message }, template: SIZES_TEMPLATE }), + parameters: { + docs: { + controls: { disable: true }, + description: { + story: + 'Both size tokens. `small` is a 32px banner with 8px inline padding and `.text-label-sm` copy; `medium` (the default) is 36px with 12px inline padding and `.text-label-md`. Block padding stays 6px in both, and the leading icon is 14px at either size. A trailing action or close control tightens the end edge to 8px, takes the matching Button/IconButton size, and lets the banner grow past the floor.' + }, + source: { code: toSfc(IMPORT, SIZES_TEMPLATE) } + } + } +} + +const WITH_LINKS_TEMPLATE = `
+ + Requests are now served from 4 new edge locations. + See the full list. + + + This workload is close to its request limit. Review the + usage report + or + upgrade the plan + to avoid throttling. + +
` + +/** @type {import('@storybook/vue3').StoryObj} */ +export const WithLinks = { + render: () => ({ components: { Message }, template: WITH_LINKS_TEMPLATE }), + parameters: { + docs: { + controls: { disable: true }, + description: { + story: + 'Copy passed through the default slot may carry inline anchors. The message region styles every descendant `a` with the `.text-link` token — underlined, token-colored, and focusable — so a plain `` needs no wrapper component.' + }, + source: { code: toSfc(IMPORT, WITH_LINKS_TEMPLATE) } + } + } +} + const CLOSABLE_MARKUP = `` @@ -206,8 +264,7 @@ export const Closable = { const AUTO_DISMISS_MARKUP = `` diff --git a/apps/storybook/src/stories/templates/DeleteDomainDialog.stories.js b/apps/storybook/src/stories/templates/DeleteDomainDialog.stories.js index df003bafd..ecb5e3f71 100644 --- a/apps/storybook/src/stories/templates/DeleteDomainDialog.stories.js +++ b/apps/storybook/src/stories/templates/DeleteDomainDialog.stories.js @@ -85,7 +85,7 @@ const SNIPPET_MARKUP = ` - +

The selected Domain will be deleted, along with all associated settings or instances. Check the

@@ -318,7 +318,7 @@ const meta = { defaultValue: { summary: "'Delete Domain'" } } }, - warningTitle: { + warningLabel: { control: 'text', description: 'Warning message shown at the top of the body.', table: { @@ -411,7 +411,7 @@ const meta = { size: 'medium', triggerLabel: 'Delete domain', title: 'Delete Domain', - warningTitle: "Once confirmed, this action can't be reversed.", + warningLabel: "Once confirmed, this action can't be reversed.", descriptionBeforeLink: 'The selected Domain will be deleted, along with all associated settings or instances. Check the ', descriptionAfterLink: ' for more details.', diff --git a/packages/webkit/catalog.json b/packages/webkit/catalog.json index c44f917f9..84a9111db 100644 --- a/packages/webkit/catalog.json +++ b/packages/webkit/catalog.json @@ -4172,18 +4172,18 @@ "doc": "Visual severity variant (maps Error to danger)." }, { - "name": "title", - "type": "string", - "default": "—", - "required": "yes", - "doc": "Primary message heading." + "name": "size", + "type": "'small' | 'medium'", + "default": "'medium'", + "required": "no", + "doc": "Size token. Drives the banner height, inline padding, copy scale, and the trailing control sizes." }, { - "name": "description", + "name": "label", "type": "string", "default": "''", "required": "no", - "doc": "Supporting body copy below the title." + "doc": "Fallback message copy when the default slot is empty." }, { "name": "icon", @@ -4197,7 +4197,7 @@ "type": "string", "default": "''", "required": "no", - "doc": "Label for the built-in text action button; hidden when empty." + "doc": "Label for the built-in secondary action button; hidden when empty." }, { "name": "closable", @@ -4235,10 +4235,10 @@ { "name": "default", "scope": "—", - "notes": "Replaces the default icon + title + description layout." + "notes": "Message copy. Accepts inline content — plain text plus anchors, which the message region styles with the `.text-link` token. Falls back to `label` when empty." } ], - "purpose": "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)." + "purpose": "Inline feedback banner that communicates status, alerts, or progress. Presents a severity-colored surface with a leading icon and a single line of message copy that may carry inline links, plus an optional secondary action button." }, "empty-state": { "import": "@aziontech/webkit/empty-state", diff --git a/packages/webkit/cli-templates/claude/skills/webkit-form/SKILL.md b/packages/webkit/cli-templates/claude/skills/webkit-form/SKILL.md index 31f0dd16d..720df57a9 100644 --- a/packages/webkit/cli-templates/claude/skills/webkit-form/SKILL.md +++ b/packages/webkit/cli-templates/claude/skills/webkit-form/SKILL.md @@ -259,11 +259,11 @@ link on the left and Save on the right. Canonical pattern: - + + To use the toolbar in production your team members need the Chrome extension, or + enable the toolbar for that domain + in the toolbar menu. + @@ -338,7 +338,7 @@ Canonical pattern:

The selected Application will be deleted, along with all associated settings or diff --git a/packages/webkit/cli-templates/claude/skills/webkit-ui-states/SKILL.md b/packages/webkit/cli-templates/claude/skills/webkit-ui-states/SKILL.md index 487fe16b0..d13112982 100644 --- a/packages/webkit/cli-templates/claude/skills/webkit-ui-states/SKILL.md +++ b/packages/webkit/cli-templates/claude/skills/webkit-ui-states/SKILL.md @@ -163,7 +163,7 @@ The state-matrix render — Skeleton (data in), then Message / EmptyState / data ' } + }) + + expect(getByTestId('feedback-message').hasAttribute('data-trailing')).toBe(true) + }) }) it('normalizes severity="error" to danger and uses the alert role', () => { const { getByTestId } = render(Message, { - props: { severity: 'error', title: 'Boom' } + props: { severity: 'error', label: 'Boom' } }) const root = getByTestId('feedback-message') @@ -49,7 +97,7 @@ describe('Message', () => { it('uses the alert role for warning severity', () => { const { getByTestId } = render(Message, { - props: { severity: 'warning', title: 'Careful' } + props: { severity: 'warning', label: 'Careful' } }) const root = getByTestId('feedback-message') @@ -57,9 +105,9 @@ describe('Message', () => { expect(root.getAttribute('role')).toBe('alert') }) - it('applies an icon override on the leading glyph', () => { + it('renders the leading icon on every severity, and applies an icon override', () => { const { getByTestId } = render(Message, { - props: { title: 'With icon', icon: 'pi pi-star' } + props: { label: 'With icon', icon: 'pi pi-star' } }) const glyph = getByTestId('feedback-message').querySelector('i') @@ -68,10 +116,57 @@ describe('Message', () => { expect(glyph?.getAttribute('aria-hidden')).toBe('true') }) + describe('message content', () => { + it('renders default-slot content in place of the label', () => { + const { getByTestId } = render(Message, { + props: { label: 'Ignored label' }, + slots: { default: 'Slot copy' } + }) + + const content = getByTestId('feedback-message__content') + expect(content.textContent).toContain('Slot copy') + expect(content.textContent).not.toContain('Ignored label') + }) + + it('keeps the leading icon when the default slot is used', () => { + const { getByTestId } = render(Message, { + slots: { default: 'Slot copy' } + }) + + expect(getByTestId('feedback-message').querySelector('i')).toBeTruthy() + }) + + it('renders inline anchors from the default slot as real, focusable links', async () => { + const { getByTestId } = render(Message, { + props: { severity: 'warning' }, + slots: { + default: 'Close to the limit. Upgrade the plan to continue.' + } + }) + + const content = getByTestId('feedback-message__content') + const anchor = content.querySelector('a') + + expect(anchor).toBeTruthy() + expect(anchor?.getAttribute('href')).toBe('/billing') + expect(anchor?.textContent).toContain('Upgrade the plan') + + // The anchor is reachable by keyboard — it is a real link, not inert markup. + anchor?.focus() + expect(document.activeElement).toBe(anchor) + }) + + // The visual side of the inline-link contract (`.text-link` colour + underline, + // applied via the content region's descendant-anchor rules) is NOT asserted here: + // this env loads no theme CSS and runs no Tailwind, so a computed-style check + // would only read the browser's default anchor styling and pass either way. + // It is verified in Storybook (the WithLinks story) per src/test/setup.ts. + }) + describe('action button', () => { it('renders the action button when actionLabel is set and emits "action" with the click event on click', async () => { const { getByTestId, emitted } = render(Message, { - props: { title: 'Has action', actionLabel: 'Retry' } + props: { label: 'Has action', actionLabel: 'Retry' } }) const action = getByTestId('feedback-message__action') @@ -86,9 +181,19 @@ describe('Message', () => { it('does not render the action button when actionLabel is empty', () => { const { queryByTestId } = render(Message, { - props: { title: 'No action' } + props: { label: 'No action' } + }) + + expect(queryByTestId('feedback-message__action')).toBeNull() + }) + + it('replaces the built-in button with the action slot', () => { + const { getByText, queryByTestId } = render(Message, { + props: { label: 'Custom action', actionLabel: 'Ignored' }, + slots: { action: '' } }) + expect(getByText('Do it')).toBeTruthy() expect(queryByTestId('feedback-message__action')).toBeNull() }) }) @@ -96,7 +201,7 @@ describe('Message', () => { describe('close / dismiss', () => { it('renders a close control when closable and emits "close" after the dismiss click', async () => { const { getByTestId, queryByTestId, emitted } = render(Message, { - props: { title: 'Dismiss me', closable: true }, + props: { label: 'Dismiss me', closable: true }, ...realTransition }) @@ -117,7 +222,7 @@ describe('Message', () => { it('does not render a close control when not closable', () => { const { queryByTestId } = render(Message, { - props: { title: 'Persistent' } + props: { label: 'Persistent' } }) expect(queryByTestId('feedback-message__close')).toBeNull() @@ -125,7 +230,7 @@ describe('Message', () => { it('dismisses on Escape when closable, emitting "close"', async () => { const { getByTestId, emitted } = render(Message, { - props: { title: 'Escapable', closable: true }, + props: { label: 'Escapable', closable: true }, ...realTransition }) @@ -139,7 +244,7 @@ describe('Message', () => { it('does not dismiss on Escape when not closable', async () => { const { getByTestId, emitted } = render(Message, { - props: { title: 'No escape' } + props: { label: 'No escape' } }) const root = getByTestId('feedback-message') @@ -150,17 +255,17 @@ describe('Message', () => { expect(emitted('close')).toBeUndefined() expect(getByTestId('feedback-message')).toBeTruthy() }) - }) - it('renders default-slot content in place of the title/description block', () => { - const { getByText, queryByTestId } = render(Message, { - props: { title: 'Ignored title', description: 'Ignored description' }, - slots: { default: 'Custom slot body' } - }) + it('auto-dismisses once life expires', async () => { + const { emitted } = render(Message, { + props: { label: 'Fleeting', life: 60 }, + ...realTransition + }) - expect(getByText('Custom slot body')).toBeTruthy() - expect(queryByTestId('feedback-message__title')).toBeNull() - expect(queryByTestId('feedback-message__description')).toBeNull() + await waitFor(() => { + expect(emitted('close')).toHaveLength(1) + }) + }) }) describe('accessibility', () => { @@ -168,8 +273,7 @@ describe('Message', () => { const { container } = render(Message, { props: { severity: 'info', - title: 'Accessible info', - description: 'A brief description of the message.', + label: 'Your workload finished deploying in 42 seconds.', actionLabel: 'Label', closable: true } @@ -180,10 +284,18 @@ describe('Message', () => { it('has no a11y violations for the danger alert variant', async () => { const { container } = render(Message, { - props: { - severity: 'danger', - title: 'Accessible alert', - description: 'Something went wrong.' + props: { severity: 'danger', label: 'The last deploy failed before reaching the edge.' } + }) + + await expectNoA11yViolations(container) + }) + + it('has no a11y violations for copy carrying inline links', async () => { + const { container } = render(Message, { + props: { severity: 'warning' }, + slots: { + default: + 'This workload is close to its request limit. Review the usage report to avoid throttling.' } }) @@ -195,7 +307,7 @@ describe('Message', () => { 'renders severity=%s with a data-severity attribute', (severity) => { const { getByTestId } = render(Message, { - props: { severity, title: `Severity ${severity}` } + props: { severity, label: `Severity ${severity}` } }) const expected = severity === 'error' ? 'danger' : severity @@ -207,6 +319,8 @@ describe('Message', () => { const { getByTestId } = render(Default()) expect(getByTestId('feedback-message')).toBeTruthy() - expect(getByTestId('feedback-message__title').textContent).toContain('Info message') + expect(getByTestId('feedback-message__content').textContent).toContain( + 'Your workload finished deploying in 42 seconds.' + ) }) }) diff --git a/packages/webkit/src/components/feedback/message/message.vue b/packages/webkit/src/components/feedback/message/message.vue index 673583c3d..e1271e324 100644 --- a/packages/webkit/src/components/feedback/message/message.vue +++ b/packages/webkit/src/components/feedback/message/message.vue @@ -10,6 +10,7 @@ } from './presets/transitions' export type MessageSeverity = 'info' | 'success' | 'warning' | 'danger' | 'error' + export type MessageSize = 'small' | 'medium' defineOptions({ name: 'Message', @@ -19,13 +20,13 @@ interface Props { /** Visual severity variant (maps Error to danger). */ severity?: MessageSeverity - /** Primary message heading. */ - title: string - /** Supporting body copy below the title. */ - description?: string + /** Size token. Drives the banner height, inline padding, copy scale, and the trailing control sizes. */ + size?: MessageSize + /** Fallback message copy when the default slot is empty. */ + label?: string /** PrimeIcons class override for the leading icon. */ icon?: string - /** Label for the built-in text action button; hidden when empty. */ + /** Label for the built-in secondary action button; hidden when empty. */ actionLabel?: string /** When true, shows a close control that dismisses the message. */ closable?: boolean @@ -35,7 +36,8 @@ const props = withDefaults(defineProps(), { severity: 'info', - description: '', + size: 'medium', + label: '', icon: '', actionLabel: '', closable: false, @@ -47,7 +49,7 @@ close: [] }>() - defineSlots<{ + const slots = defineSlots<{ default(): unknown action(): unknown }>() @@ -86,6 +88,12 @@ const resolvedIcon = computed(() => props.icon || defaultIcons[normalizedSeverity.value]) + // A trailing control (action button or close) brings its own inline padding, so the + // banner tightens its end edge to keep the optical inset even. + const hasTrailingControl = computed(() => + Boolean(props.actionLabel || slots.action || props.closable) + ) + const role = computed(() => normalizedSeverity.value === 'danger' || normalizedSeverity.value === 'warning' ? 'alert' @@ -158,40 +166,32 @@ :role="role" :data-testid="testId" :data-severity="normalizedSeverity" + :data-size="size" + :data-trailing="hasTrailingControl || null" :style="dismissTransitionStyle" tabindex="-1" - class="flex min-h-14 items-center gap-[var(--spacing-sm)] rounded-[var(--shape-button)] border border-[length:var(--border-width-default,1px)] p-[var(--spacing-sm)] shadow-[var(--shadow-xs)] data-[severity=info]:border-[var(--info-border)] data-[severity=info]:bg-[var(--info)] data-[severity=success]:border-[var(--success-border)] data-[severity=success]:bg-[var(--success)] data-[severity=warning]:border-[var(--warning-border)] data-[severity=warning]:bg-[var(--warning)] data-[severity=danger]:border-[var(--danger-border)] data-[severity=danger]:bg-[var(--danger)]" + class="relative box-border flex w-full flex-wrap items-center gap-[var(--spacing-sm)] break-words rounded-[var(--shape-button)] border border-[length:var(--border-width-default,1px)] py-1.5 shadow-[var(--shadow-xs)] data-[size=small]:min-h-8 data-[size=small]:px-[var(--spacing-xs)] data-[size=medium]:min-h-9 data-[size=medium]:px-[var(--spacing-sm)] data-[trailing]:pr-[var(--spacing-xs)] data-[severity=info]:border-[var(--info-border)] data-[severity=info]:bg-[var(--info)] data-[severity=success]:border-[var(--success-border)] data-[severity=success]:bg-[var(--success)] data-[severity=warning]:border-[var(--warning-border)] data-[severity=warning]:bg-[var(--warning)] data-[severity=danger]:border-[var(--danger-border)] data-[severity=danger]:bg-[var(--danger)]" @keydown="handleEscape" > - - +