srcdev-nuxt-components 9.6.9 → 9.6.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +4 -2
  2. package/.claude/skills/component-compliance-checklist.md +24 -2
  3. package/.claude/skills/component-inline-action-button.md +5 -4
  4. package/.claude/skills/component-naming.md +8 -2
  5. package/.claude/skills/components/{alert-mask-core.md → alert-mask.md} +38 -23
  6. package/.claude/skills/components/alert-masked-content.md +3 -3
  7. package/.claude/skills/components/container-glow.md +1 -1
  8. package/.claude/skills/components/display-theme-switch.md +8 -8
  9. package/.claude/skills/components/input-button.md +26 -7
  10. package/.claude/skills/components/input-checkbox-radio.md +2 -0
  11. package/.claude/skills/components/input-description.md +5 -0
  12. package/.claude/skills/components/input-error.md +9 -2
  13. package/.claude/skills/components/input-label.md +8 -2
  14. package/.claude/skills/components/input-number.md +2 -0
  15. package/.claude/skills/components/input-otp.md +2 -0
  16. package/.claude/skills/components/input-range.md +2 -0
  17. package/.claude/skills/components/input-select.md +2 -0
  18. package/.claude/skills/components/input-text.md +2 -0
  19. package/.claude/skills/components/input-textarea.md +2 -0
  20. package/.claude/skills/components/pending-effect.md +5 -4
  21. package/.claude/skills/components/{triple-toggle-switch.md → segmented-control.md} +34 -19
  22. package/.claude/skills/components/services-card.md +1 -1
  23. package/.claude/skills/components/tabbed-content.md +1 -1
  24. package/.claude/skills/components/toggle-switch.md +2 -0
  25. package/.claude/skills/css-nesting-conventions.md +22 -1
  26. package/.claude/skills/index.md +5 -5
  27. package/.claude/skills/storybook-add-story.md +3 -1
  28. package/.claude/skills/theming-component-token-pattern.md +24 -0
  29. package/.claude/skills/theming-form-geometry-tokens.md +1 -1
  30. package/.claude/skills/theming-partial-override.md +1 -1
  31. package/.vscode/srcdev-component-alert-mask.code-snippets +41 -0
  32. package/.vscode/{srcdev-component-triple-toggle-switch.code-snippets → srcdev-component-segmented-control.code-snippets} +6 -7
  33. package/app/assets/styles/setup/03.theming/_theme-slots.css +1 -1
  34. package/app/assets/styles/setup/04.elements/forms/03.generic-input-geometry.css +4 -0
  35. package/app/components/01.atoms/alert-mask/AlertMask.vue +194 -0
  36. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +57 -42
  37. package/app/components/01.atoms/alert-mask/stories/AlertMask.stories.ts +198 -0
  38. package/app/components/01.atoms/alert-mask/tests/AlertMask.spec.ts +233 -0
  39. package/app/components/02.molecules/alert-masked-content/AlertMaskedContent.vue +2 -2
  40. package/app/components/02.molecules/alert-masked-content/CONSUMER-STYLING.md +4 -4
  41. package/app/components/02.molecules/alert-masked-content/stories/AlertMaskedContent.stories.ts +60 -0
  42. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +12 -12
  43. package/app/components/02.molecules/cookie-consent-banner/CONSUMER-STYLING.md +3 -3
  44. package/app/components/02.molecules/cookie-consent-banner/CookieConsentBanner.vue +1 -1
  45. package/app/components/02.molecules/cookie-consent-banner/tests/CookieConsentBanner.spec.ts +1 -1
  46. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +9 -9
  47. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +16 -16
  48. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +53 -1
  49. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +8 -8
  50. package/app/components/02.molecules/input-copy/CONSUMER-STYLING.md +4 -4
  51. package/app/components/02.molecules/input-copy/InputCopy.vue +2 -2
  52. package/app/components/02.molecules/input-copy/tests/InputCopy.spec.ts +2 -2
  53. package/app/components/03.organisms/google-reviews/tests/GoogleReviews.spec.ts +4 -4
  54. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +2 -2
  55. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +2 -2
  56. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +6 -6
  57. package/app/components/05.forms/form-errors/CONSUMER-STYLING.md +8 -0
  58. package/app/components/05.forms/form-errors/InputError.vue +43 -37
  59. package/app/components/05.forms/form-errors/stories/InputError.stories.ts +79 -1
  60. package/app/components/05.forms/form-errors/tests/InputError.spec.ts +29 -1
  61. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +80 -47
  62. package/app/components/05.forms/input-button/InputButton.vue +52 -100
  63. package/app/components/05.forms/input-button/playwright/input-button.playwright.ts +1 -1
  64. package/app/components/05.forms/input-button/stories/InputButton.stories.ts +145 -8
  65. package/app/components/05.forms/input-button/tests/InputButton.spec.ts +76 -46
  66. package/app/components/05.forms/input-button/tests/__snapshots__/InputButton.spec.ts.snap +17 -17
  67. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +9 -1
  68. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +8 -0
  69. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +2 -2
  70. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +14 -2
  71. package/app/components/05.forms/input-description/InputDescription.vue +2 -1
  72. package/app/components/05.forms/input-description/stories/InputDescription.stories.ts +70 -4
  73. package/app/components/05.forms/input-description/tests/InputDescription.spec.ts +2 -1
  74. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +10 -2
  75. package/app/components/05.forms/input-label/InputLabel.vue +1 -0
  76. package/app/components/05.forms/input-label/stories/InputLabel.stories.ts +68 -2
  77. package/app/components/05.forms/input-label/tests/InputLabel.spec.ts +13 -3
  78. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +7 -1
  79. package/app/components/05.forms/input-number/InputNumber.vue +4 -5
  80. package/app/components/05.forms/input-otp/CONSUMER-STYLING.md +7 -1
  81. package/app/components/05.forms/input-otp/InputOtp.vue +2 -2
  82. package/app/components/05.forms/input-range/InputRange.vue +1 -1
  83. package/app/components/05.forms/input-select/CONSUMER-STYLING.md +7 -1
  84. package/app/components/05.forms/input-select/InputSelect.vue +14 -5
  85. package/app/components/05.forms/input-text/CONSUMER-STYLING.md +7 -1
  86. package/app/components/05.forms/input-text/InputText.vue +4 -4
  87. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +7 -1
  88. package/app/components/05.forms/input-textarea/InputTextarea.vue +2 -3
  89. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +4 -4
  90. package/app/components/05.forms/pending-effect/CONSUMER-STYLING.md +7 -3
  91. package/app/components/05.forms/pending-effect/PendingEffect.vue +3 -3
  92. package/app/components/05.forms/pending-effect/tests/PendingEffect.spec.ts +1 -1
  93. package/app/components/05.forms/segmented-control/CONSUMER-STYLING.md +125 -0
  94. package/app/components/05.forms/segmented-control/SegmentedControl.vue +231 -0
  95. package/app/components/05.forms/segmented-control/stories/SegmentedControl.stories.ts +215 -0
  96. package/app/components/05.forms/segmented-control/tests/SegmentedControl.spec.ts +202 -0
  97. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +7 -0
  98. package/app/components/05.forms/toggle-switch/ToggleSwitch.vue +2 -0
  99. package/app/types/components/index.ts +1 -1
  100. package/package.json +1 -1
  101. package/.vscode/srcdev-component-alert-mask-core.code-snippets +0 -31
  102. package/app/components/01.atoms/alert-mask/AlertMaskCore.vue +0 -166
  103. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +0 -105
  104. package/app/components/01.atoms/alert-mask/tests/AlertMaskCore.spec.ts +0 -342
  105. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +0 -99
  106. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitch.vue +0 -251
  107. package/app/components/05.forms/triple-toggle-switch/stories/TripleToggleSwitch.stories.ts +0 -104
  108. package/app/components/05.forms/triple-toggle-switch/tests/TripleToggleSwitch.spec.ts +0 -138
  109. /package/app/types/components/{alert-mask-core.d.ts → alert-mask.d.ts} +0 -0
@@ -6,9 +6,11 @@ paths:
6
6
  # Element ids come from `useId()`, never from data
7
7
 
8
8
  Build `id`/`for` pairs from `useId()` (plus a loop index for repeated items), never from props or
9
- data values like `` `${name}-${item.value}` ``.
9
+ data values like `` `${name}-${item.value}` ``. A radio group's default `name` comes from
10
+ `useId()` too, never a fixed string.
10
11
 
11
12
  Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
12
- an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). Test
13
+ an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). A fixed default radio `name` makes every instance on a page
14
+ one group, so picking in one clears the others (`SegmentedControl`, 2026-10-17). Test
13
15
  uniqueness with both instances inside one host component: separate `mountSuspended` calls each
14
16
  restart the `useId()` counter. Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components).
@@ -183,7 +183,8 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
183
183
  8. **VS Code snippet** — create or update `.vscode/srcdev-component-{family}.code-snippets`,
184
184
  covering the control and its Field wrapper in one file. For a teleported component, a CSS
185
185
  override scaffold must not nest its selector under a page class: it never matches (the
186
- `DisplayToast` snippet did, fixed 2026-10-16).
186
+ `DisplayToast` snippet did, fixed 2026-10-16). A tab stop with a default value is `${3:8}`:
187
+ `$38` is read as tab stop 38 and the default never appears (`AlertMask`, fixed 2026-10-17).
187
188
  9. **Accessibility** — check for gaps beyond what already-passing tests would catch:
188
189
  - Interactive controls (buttons, toggles, custom form-like widgets) have an accessible name —
189
190
  visible text, `aria-label`, or `aria-labelledby`.
@@ -196,6 +197,11 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
196
197
  - A toast, banner or status message doesn't `.focus()` itself when it appears (rule
197
198
  `consumer/a11y-no-focus-steal.md`).
198
199
  - Focus is visible (`:focus-visible`, not a suppressed outline) on anything focusable.
200
+ - `aria-invalid` is bound as `fieldHasError ? 'true' : undefined`, never the boolean itself:
201
+ `:aria-invalid="fieldHasError"` renders `aria-invalid="false"` on every valid field
202
+ (`InputTextarea`, `ToggleSwitch`, fixed 2026-10-16). Same for an empty `aria-describedby`.
203
+ - A checkbox drawn as an on/off switch has `role="switch"`, and its decorative track is
204
+ `aria-hidden` (`ToggleSwitch`, 2026-10-16).
199
205
  - Live-updating or auto-advancing content (carousels, marquees, auto-dismissing toasts) has a
200
206
  way to pause it, per WCAG 2.2.2 — a visible control, or pause-on-hover/focus at minimum.
201
207
  - `prefers-reduced-motion` is respected for any animation that isn't purely decorative.
@@ -275,13 +281,29 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
275
281
  defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
276
282
  2026-10-16).
277
283
  - Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
284
+ - A wrapper passes its child only props the child declares. Anything else falls through as an
285
+ HTML attribute on the child's root: `InputTextField` put `label="…"` and
286
+ `error-message="…"` on `.input-text-wrapper` (fixed 2026-10-16). Check the child root's
287
+ attributes in a test.
288
+ - A control styled with `all: unset` restores the UA defaults it depends on. On a `<textarea>`
289
+ that reset `white-space: pre-wrap` (inherited, so it took the page's `normal`); set
290
+ `white-space: pre-wrap` and `overflow-wrap: anywhere` explicitly (`InputTextarea`,
291
+ 2026-10-16).
292
+ - A state rule (`[data-checked]`) written at the root level loses to deeper nested base rules
293
+ for the same element. Nest it in the element's own block as `.root[data-x] & { … }`
294
+ (`ToggleSwitch`, 2026-10-16; `css-nesting-conventions.md`). Same for hover and focus: keep
295
+ both at the same nesting depth with focus after hover, or a deeper hover rule recolours the
296
+ focus ring while both apply (`InputSelect`, 2026-10-17).
278
297
  - Step buttons and any other script that changes a number round to the step's precision
279
298
  (`toFixed`), clamp to `min`/`max`, and compare limits with `<=`/`>=`, never `===` (0.1 steps
280
299
  drift to 0.30000000000000004, and an overshoot past an `===` check never disables the button).
281
300
  Reference: `stepBy()` in `InputNumberField`.
282
301
  - Layout measured in script (connector lines, indicators) re-measures when any measured item
283
302
  resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
284
- pitfall #28).
303
+ pitfall #28). Prefer a CSS-only position when one exists (rule above, `SegmentedControl`).
304
+ - Geometry drawn in script (SVG paths, canvas) is physical. Position content around it with
305
+ physical properties (`margin-left`, not `margin-inline-start`), or right-to-left content
306
+ overlaps a one-sided bar (`AlertMask`, 2026-10-17).
285
307
  - Action buttons are `InputButton` (`variant="inline"` when the wrapper styles them), not a raw
286
308
  `<button>` (`component-inline-button.md`, `component-inline-action-button.md`). The ledger
287
309
  doesn't check this, which is how `InputCopy` scored 5/5 with one (fixed 2026-10-15).
@@ -76,7 +76,7 @@ Target `.my-action-button.input-button` inside the wrapper to override InputButt
76
76
 
77
77
  ## What `variant="inline"` still brings
78
78
 
79
- `inline` adds no colours, but the base `.input-button` rule and its inner `.button-text` still
79
+ `inline` adds no colours, but the base `.input-button` rule and its inner `.input-button-text` still
80
80
  apply. Override what the wrapper doesn't want, on the `.my-action-button.input-button` selector:
81
81
 
82
82
  - `box-sizing: content-box`: set `border-box` if the button has a `max-inline-size: 100%` or sits
@@ -84,10 +84,11 @@ apply. Override what the wrapper doesn't want, on the `.my-action-button.input-b
84
84
  - `min-height: var(--button-min-height)`: set `auto` to size from your own padding.
85
85
  - `grid-auto-flow: column` layout, `overflow: hidden` and a `transition: all` (replace it with
86
86
  your own `transition` list).
87
- - `.button-text` takes `font-size: var(--button-font-size)` and
87
+ - `.input-button-text` takes `font-size: var(--button-font-size)` and
88
88
  `text-transform: var(--button-text-transform)`: set `font-size: inherit; text-transform: none`
89
- on `& .button-text` to keep the wrapper's type. It already wraps unbroken text
90
- (`overflow-wrap: anywhere`, with `white-space` from `--input-button-text-white-space`).
89
+ on `& .input-button-text` to keep the wrapper's type. It already wraps unbroken text
90
+ (`overflow-wrap: anywhere`, with `white-space` from `--input-button-text-white-space`, default
91
+ `normal`).
91
92
  - The label is the `buttonText` string prop; there's no label slot. A wrapper that had label
92
93
  slots on a raw `<button>` turns them into string props when it switches (a breaking change).
93
94
 
@@ -60,7 +60,12 @@ stripping `Core`.
60
60
  control name).
61
61
  2. `sed` the old names across `app/`, `.claude/`, `.vscode/`, `MIGRATION.md` and `CLAUDE.md`:
62
62
  component names, `import` paths, story titles/`*StoryArgs` interfaces, `describe()` names, and the
63
- root/base DOM class (update `CONSUMER-STYLING.md` and test selectors that use it).
63
+ root/base DOM class (update `CONSUMER-STYLING.md` and test selectors that use it). Match on word
64
+ boundaries (`\bToggleSwitchCore\b`): a bare `ToggleSwitchCore` also matched inside
65
+ `TripleToggleSwitchCore` and rewrote its history notes (2026-10-16). Leave out the gitignored
66
+ `.claude/settings.json` (its old allowlist entries match too) and any dated "Renamed"/"Changed"
67
+ notes, and rename a removed wrapper by hand before the bulk pass, or `XWithLabel` → `XField`
68
+ turns `XWithLabelInline` into a name that never existed.
64
69
  3. Add a "Renamed YYYY-MM-DD" note at the top of the skill doc listing old → new names and classes.
65
70
  4. Grep consumer repos (`/check-component-usage`) and **list** the files/lines that use the old
66
71
  names (in the private consumer notes, never in this repo). Don't edit the consumers yet: they pin
@@ -93,12 +98,13 @@ external consumer, free to rename. Run `/check-component-usage` for the current
93
98
  | ~~ToggleSwitchWithLabel~~ | ✅ `ToggleSwitchField` (2026-10-16) | yes |
94
99
  | ~~ToggleSwitchWithLabelInline~~ | ✅ `layout="inline"` on `ToggleSwitchField` (2026-10-16) | - |
95
100
  | ~~TripleToggleSwitchCore~~ | ✅ `TripleToggleSwitch` (2026-09-27) | - |
101
+ | ~~TripleToggleSwitch~~ | ✅ `SegmentedControl` (2026-10-17): handles any option count | - |
96
102
  | ~~InputButtonCore~~ | ✅ `InputButton` (2026-09-28) | yes |
97
103
  | ~~InputCheckboxRadioCore~~ | ✅ `InputCheckboxRadio` (2026-09-27) | - |
98
104
  | InputCheckboxRadioButton | ✅ kept: button-style option, not a Field (2026-09-27) | - |
99
105
  | ~~InputCheckboxRadioWithLabel~~ | ✅ `InputCheckboxRadioField` (2026-09-27) | - |
100
106
  | ~~ActionMenuItemCore~~ | ✅ `ActionMenuItem` (2026-09-27) | - |
101
- | AlertMaskCore | `AlertMask` | - |
107
+ | ~~AlertMaskCore~~ | ✅ `AlertMask` (2026-10-17) | - |
102
108
  | CardCore | **decide**: `Card` is single-word and collision-prone; prefer a prefixed name (`Display*`, `Content*`) | yes |
103
109
  | AccordianCore | **decide**: also fixes the "Accordian" misspelling; bare `Accordion` is single-word and collision-prone, and it is built on `<details>`/`<summary>` | yes |
104
110
  | ~~TabsCore~~ | ✅ `TabbedContent` (2026-10-07) | - |
@@ -1,14 +1,18 @@
1
1
  ---
2
- name: AlertMaskCore
3
- description: AlertMaskCore SVG border/background mask sized to slotted content; config-prop-driven geometry/colour, no CSS token API
2
+ name: AlertMask
3
+ description: AlertMask SVG border and fill sized to slotted content; config-prop geometry, --alert-mask-* colour tokens
4
4
  type: reference
5
5
  ---
6
6
 
7
- # AlertMaskCore
7
+ # AlertMask
8
+
9
+ > **Renamed 2026-10-17**: `AlertMaskCore` → `AlertMask` (Control/Field naming), root class
10
+ > `.alert-mask-core` → `.alert-mask`, types file `alert-mask-core.d.ts` → `alert-mask.d.ts`
11
+ > (`AlertMaskConfig` unchanged). Props and slot unchanged.
8
12
 
9
13
  ## Overview
10
14
 
11
- `AlertMaskCore` draws a border + background around its default slot content using an SVG `<mask>`,
15
+ `AlertMask` draws a border + background around its default slot content using an SVG `<mask>`,
12
16
  sized to the content's measured dimensions via `ResizeObserver`. It's the low-level primitive
13
17
  `AlertMaskedContent` (`app/components/02.molecules/alert-masked-content/`) builds on for its
14
18
  accent-bordered alert shape.
@@ -20,12 +24,12 @@ accent-bordered alert shape.
20
24
  | `config` | `AlertMaskConfig` | `{}` | Border/background colour and geometry (see below). All fields optional. |
21
25
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
22
26
 
23
- ### `AlertMaskConfig` (`app/types/components/alert-mask-core.d.ts`)
27
+ ### `AlertMaskConfig` (`app/types/components/alert-mask.d.ts`)
24
28
 
25
29
  | Field | Type | Default | Description |
26
30
  |---|---|---|---|
27
- | `backgroundColour` | `string` | `"rgba(0,0,0,0.25)"` | Inner fill colour. |
28
- | `borderColour` | `string` | `"var(--orange-08)"` | Border fill colour. |
31
+ | `backgroundColour` | `string` | token `--alert-mask-background-colour` | Inner fill colour; overrides the token. |
32
+ | `borderColour` | `string` | token `--alert-mask-border-colour` | Border fill colour; overrides the token. |
29
33
  | `radiusLeft` | `number` | `12` | Left-side corner radius (px). |
30
34
  | `radiusRight` | `number` | `12` | Right-side corner radius (px). |
31
35
  | `borderLeft` | `number` | `8` | Left border thickness (px). |
@@ -33,14 +37,18 @@ accent-bordered alert shape.
33
37
  | `borderRight` | `number` | `8` | Right border thickness (px). |
34
38
  | `borderBottom` | `number` | `8` | Bottom border thickness (px). |
35
39
 
40
+ Sides are physical, matching the drawn path. Negative or non-finite sizes fall back to `0` and the
41
+ default respectively; radii are clamped to half the box's smaller side (inner radii to the inner
42
+ box), and borders that leave no inner box drop the fill.
43
+
36
44
  ## Basic usage
37
45
 
38
46
  ```vue
39
- <AlertMaskCore>
47
+ <AlertMask>
40
48
  <p>Alert content</p>
41
- </AlertMaskCore>
49
+ </AlertMask>
42
50
 
43
- <AlertMaskCore
51
+ <AlertMask
44
52
  :config="{
45
53
  borderColour: 'var(--theme-accent)',
46
54
  backgroundColour: 'rgba(0, 0, 0, 0.3)',
@@ -53,33 +61,33 @@ accent-bordered alert shape.
53
61
  }"
54
62
  >
55
63
  <p>Accent-bordered alert content</p>
56
- </AlertMaskCore>
64
+ </AlertMask>
57
65
  ```
58
66
 
59
67
  ## How it works
60
68
 
61
69
  - The default-slot content is measured via `ResizeObserver` (initial measurement runs after
62
- `nextTick` on mount).
70
+ `nextTick` on mount). Only the content size is stored; the SVG size is computed from it plus the
71
+ current borders, so a `config` change redraws without waiting for a resize. Nothing draws until
72
+ the first measurement.
63
73
  - An SVG outer path (rounded rect using `radiusLeft`/`radiusRight`) and inner path (the same rect
64
74
  inset by the border thicknesses) are computed from that measurement, and the region between them
65
75
  is masked to `borderColour`; the inner region is filled with `backgroundColour`.
66
76
  - Each instance gets its own mask id (`alert-mask-<useId>`), so several masks on one page don't
67
77
  share a shape. The SVG is `aria-hidden`.
68
- - `borderColour`/`backgroundColour` are applied as `fill` **styles**, not attributes, so a `var()`
69
- value (e.g. `var(--theme-accent)`, or a consumer token) resolves against the element's ancestors.
70
- A `var(--theme-*)` colour needs a `data-theme` ancestor to resolve.
78
+ - Colours come from `--alert-mask-border-colour` / `--alert-mask-background-colour` (theme
79
+ defaults). A `config` colour is applied as an inline `fill` **style**, so it wins and a `var()`
80
+ value resolves against the element's ancestors.
71
81
  - The `ResizeObserver` is disconnected on unmount.
72
82
  - The slot content itself sits in a separate `.alert-mask-content` div, inset by the same border
73
- thicknesses via internal `--_inset-*` custom properties, so it visually sits inside the drawn
74
- border without overlapping it.
83
+ thicknesses via private `--_inset-left|right|top|bottom` (physical `margin`, so right-to-left
84
+ layouts keep the content clear of a left accent bar).
75
85
 
76
- ## No CSS token API
86
+ ## Styling
77
87
 
78
- Unlike most components in this library, `AlertMaskCore` has no public `--alert-mask-core-*` CSS
79
- custom properties — the SVG path geometry can't be driven by `var()`, so `config` is the entire
80
- override surface (colour included). Colour strings may be `var()` references, so a wrapper
81
- component can expose its own tokens through them (as `AlertMaskedContent` does with
82
- `--alert-masked-content-border-colour`/`-background`). See CONSUMER-STYLING.md.
88
+ Colour tokens `--alert-mask-border-colour` and `--alert-mask-background-colour`; geometry through
89
+ `config`. `AlertMaskedContent` passes both colours through `config` from its own
90
+ `--alert-masked-content-*` tokens. See CONSUMER-STYLING.md.
83
91
 
84
92
  ## Notes
85
93
 
@@ -96,3 +104,10 @@ component can expose its own tokens through them (as `AlertMaskedContent` does w
96
104
  resolves reliably, and the `ResizeObserver` is disconnected on unmount (it previously leaked).
97
105
  Stories now render over a photo/gradient backdrop with a `data-theme` wrapper; on the plain canvas
98
106
  they looked flat grey.
107
+ - 2026-10-17 migration: renamed from `AlertMaskCore`. Fixed: content margins used `0` when
108
+ `config` omitted a border while the path used the default `8` (content overlapped the border);
109
+ the SVG size only updated on a content resize, so a `config` border change left it stale; no
110
+ clamping, so an oversized radius or negative border drew a broken path; content margins were
111
+ logical while the path is physical (right-to-left content overlapped the left accent bar);
112
+ passthrough didn't react. Colours became `--alert-mask-*` tokens with `--theme-*` defaults.
113
+ The snippet's tab stops (`$38` read as stop 38) were fixed. `StressTest` story added.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: AlertMaskedContent
3
- description: AlertMaskedContent — AlertContentInner inside AlertMaskCore's SVG mask (accent border cut-out + translucent fill); the masked variant DisplayToast/DisplayPrompt swap in
3
+ description: AlertMaskedContent — AlertContentInner inside AlertMask's SVG mask (accent border cut-out + translucent fill); the masked variant DisplayToast/DisplayPrompt swap in
4
4
  type: reference
5
5
  ---
6
6
 
@@ -9,7 +9,7 @@ type: reference
9
9
  ## Overview
10
10
 
11
11
  The "glass" sibling of `AlertContent`. Same icon/title/body/dismiss layout (the shared
12
- `AlertContentInner`), but instead of an opaque surface it sits inside `AlertMaskCore`, which draws
12
+ `AlertContentInner`), but instead of an opaque surface it sits inside `AlertMask`, which draws
13
13
  an accent-coloured border as an SVG cut-out and a translucent fill, so the page behind shows
14
14
  through.
15
15
 
@@ -91,5 +91,5 @@ See `CONSUMER-STYLING.md` next to the component. Public tokens:
91
91
  - Changed 2026-09-27: colours are now overridable via the two public tokens (previously fixed
92
92
  literals, reachable only through `maskConfig`), and the transparent inner background uses
93
93
  `AlertContentInner`'s public `--alert-content-inner-background` instead of reaching into its
94
- private `--_alert-content-inner-bg`. `AlertMaskCore` now gives each instance a unique mask id,
94
+ private `--_alert-content-inner-bg`. `AlertMask` now gives each instance a unique mask id,
95
95
  so several masked alerts on one page (e.g. stacked toasts) no longer all use the first one's shape.
@@ -97,7 +97,7 @@ apply instantly instead of fading).
97
97
 
98
98
  - 2026-09-20 migration: renamed from `ContainerGlowCore` to `ContainerGlow` — the `Core` suffix in
99
99
  this library denotes a primitive with a non-Core wrapper built on top of it (`InputTextCore` → `InputTextWithLabel`, since renamed;
100
- `AlertMaskCore` → `AlertMaskedContent`), and this component has no such
100
+ `AlertMask` → `AlertMaskedContent`), and this component has no such
101
101
  sibling. Checked all consumer repos first; found one real usage in a defunct
102
102
  demo app, left as a follow-up for that repo rather than edited here.
103
103
  - Moved from `app/components/container-glow/` (unplaced) into
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: DisplayThemeSwitch
3
- description: DisplayThemeSwitch system/light/dark theme picker wired to useSettingsStore, wraps TripleToggleSwitch
3
+ description: DisplayThemeSwitch system/light/dark theme picker wired to useSettingsStore, wraps SegmentedControl
4
4
  type: reference
5
5
  ---
6
6
 
@@ -9,7 +9,7 @@ type: reference
9
9
  ## Overview
10
10
 
11
11
  `DisplayThemeSwitch` is a system/light/dark colour-scheme picker. It's a thin wrapper around
12
- `TripleToggleSwitch` (`app/components/05.forms/triple-toggle-switch/`), wired to
12
+ `SegmentedControl` (`app/components/05.forms/segmented-control/`), wired to
13
13
  `useSettingsStore` (`app/stores/useSettingsStore.ts`) so selecting an option updates the app's
14
14
  `colourScheme` state and applies the matching class (`system`/`light`/`dark`) to
15
15
  `<html>`. Renders inside `<ClientOnly>` since the store's persisted value isn't known until
@@ -43,8 +43,8 @@ hydration.
43
43
  ## Styling
44
44
 
45
45
  No `--display-theme-switch-*` tokens of its own — every visual aspect (colour, marker gradient,
46
- sizing) is delegated to `TripleToggleSwitch`'s public `--triple-toggle-switch-*` tokens. See
47
- CONSUMER-STYLING.md here and `TripleToggleSwitch`'s own CONSUMER-STYLING.md.
46
+ sizing) is delegated to `SegmentedControl`'s public `--segmented-control-*` tokens. See
47
+ CONSUMER-STYLING.md here and `SegmentedControl`'s own CONSUMER-STYLING.md.
48
48
 
49
49
  ## Notes
50
50
 
@@ -52,10 +52,10 @@ CONSUMER-STYLING.md here and `TripleToggleSwitch`'s own CONSUMER-STYLING.md.
52
52
  `02.molecules/`. Added `systemLabel`/`lightLabel`/`darkLabel`/`systemIcon`/`lightIcon`/`darkIcon`
53
53
  props — the three option labels/icons were previously hardcoded English strings with no override
54
54
  hook. Removed a large block of dead/duplicate CSS in this component's own `<style>` that either
55
- no-op-duplicated `TripleToggleSwitch`'s own default styling (same selectors, same values) or
56
- actively bypassed its public `--triple-toggle-switch-*` tokens with hardcoded literals (a
57
- consumer setting e.g. `--triple-toggle-switch-surface` globally would have been silently
55
+ no-op-duplicated `SegmentedControl`'s own default styling (same selectors, same values) or
56
+ actively bypassed its public `--segmented-control-*` tokens with hardcoded literals (a
57
+ consumer setting e.g. `--segmented-control-surface` globally would have been silently
58
58
  overridden back). Only the genuine `small`-variant sizing override remains, now driving
59
- `TripleToggleSwitch`'s own public sizing tokens (`--triple-toggle-switch-gap`/`-padding`/
59
+ `SegmentedControl`'s own public sizing tokens (`--segmented-control-gap`/`-padding`/
60
60
  `-option-padding`/`-icon-size`, added in the same migration) instead of reaching into its
61
61
  private `--_form-*`/`--_scheme-icon-font-size` locals directly.
@@ -18,14 +18,14 @@ so its tokens restyle those too.
18
18
  | Prop | Type | Default | Notes |
19
19
  |---|---|---|---|
20
20
  | `buttonText` | `string` | `""` | Visible label; becomes `sr-only` with the `#iconOnly` slot, so always set it for icon-only buttons |
21
- | `variant` | `"primary" \| "secondary" \| "tertiary" \| "inline"` | `"primary"` | `inline` is for action buttons inside custom wrappers (see `component-inline-action-button.md`) |
21
+ | `variant` | `"primary" \| "secondary" \| "tertiary" \| "inline"` | `"primary"` | Rendered as `data-variant`. Primary is filled, secondary outlined, tertiary text-only (tint on hover). `inline` has no colours of its own and is for action buttons inside custom wrappers (see `component-inline-action-button.md`) |
22
22
  | `type` | `"button" \| "reset" \| "submit"` | `"button"` | Ignored when rendered as a link |
23
23
  | `href` | `string` | — | Renders a link instead of a button |
24
24
  | `external` | `boolean` | `false` | Force a real navigation for a same-origin `href` that isn't a Vue Router page (e.g. a Nitro server route) |
25
25
  | `theme` | `"default" \| "success" \| "error" \| "warning"` | `"default"` | Sets `data-theme`, re-declaring the `--theme-*` palette on the button |
26
- | `isPill` | `boolean` | `false` | Fully rounded |
27
- | `isPending` | `boolean` | `false` | Adds `.is-pending` |
28
- | `hasPendingEffect` | `boolean` | `false` | Renders the `PendingEffect` overlay |
26
+ | `isPill` | `boolean` | `false` | Fully rounded (`data-pill`) |
27
+ | `isPending` | `boolean` | `false` | Sets `data-pending` (dimmed primary surface; with `hasPendingEffect`, animates the border and blocks clicks) |
28
+ | `hasPendingEffect` | `boolean` | `false` | Renders the `PendingEffect` overlay, sets `data-pending-effect` |
29
29
  | `readonly` | `boolean` | `false` | Dimmed, `aria-disabled="true"`, and not operable: clicks (mouse or keyboard) are swallowed before any consumer `@click` runs |
30
30
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Classes on the root. Reactive after mount |
31
31
 
@@ -56,7 +56,7 @@ Give slotted `<Icon>`s `class="icon"` to pick up `--input-icon-size` (applied as
56
56
  ## Accessibility
57
57
 
58
58
  - Icon-only buttons keep `buttonText` as `sr-only` text: never leave it empty.
59
- - `readonly` is exposed as `aria-disabled="true"` and blocks activation; the button stays focusable
59
+ - `readonly` is exposed as `aria-disabled="true"` (omitted otherwise) and blocks activation; the button stays focusable
60
60
  so its label is still discoverable (unlike native `disabled`).
61
61
  - Focus ring: `:focus-visible` with `--button-focus-ring-width` / `-offset`.
62
62
 
@@ -66,10 +66,29 @@ Colour tokens are `--input-button-{variant}-{property}` with `--theme-*` fallbac
66
66
  typography are the global `--button-*` tokens. Full reference:
67
67
  `app/components/05.forms/input-button/CONSUMER-STYLING.md`.
68
68
 
69
- Labels stay on one line by default. Set `--input-button-text-white-space: normal` to let a long
70
- label wrap inside a narrow container (added 2026-10-06; `ServicesCardGrid` does this for its CTAs).
69
+ Long labels wrap inside the button (`--input-button-text-white-space` defaults to `normal`, and
70
+ unbroken strings break anywhere). Set it to `nowrap` to keep a label on one line; it then widens
71
+ the button instead. Button text is never line-clamped.
72
+
73
+ State hooks on the root: `data-variant`, `data-theme`, `data-pill`, `data-pending`,
74
+ `data-pending-effect`, `data-link`, `data-icon-only`. Inner elements: `.input-button-text` and
75
+ `.input-button-icon[data-slot="left" | "right" | "icon-only"]`.
71
76
 
72
77
  ## Notes
73
78
 
74
79
  - Before 2026-09-28, `readonly` only set `pointer-events: none`, so keyboard users could still
75
80
  activate it; the icon size (`width`/`height` on `.icon`) was silently overridden by `@nuxt/icon`.
81
+ - **2026-10-17**: secondary changed from a filled `--theme-surface-inverted` button to outlined
82
+ (transparent, `--theme-surface` border and text, `--theme-surface-subtle` on hover), and tertiary
83
+ from a grey `--slate-01` fill with an underline to text-only. Both left the `contrast-color()`
84
+ trial, which now covers primary only. Primary and secondary gained a 2px `box-shadow` ring
85
+ (`--input-button-{variant}-shadow`, default `--colour-theme-2`; width `--input-button-shadow-width`),
86
+ and their hover outline now takes the hover border colour (1px, no offset). Tertiary has neither.
87
+ - **2026-10-17**: state moved from bare classes to `data-*` attributes (`.primary` →
88
+ `[data-variant="primary"]`, `.pill` → `[data-pill]`, `.is-pending` → `[data-pending]`,
89
+ `.pending-effect` → `[data-pending-effect]`, `.is-link` → `[data-link]`, `.icon-only` →
90
+ `[data-icon-only]`), and the inner classes were prefixed (`.button-text` →
91
+ `.input-button-text`, `.btn-icon.left` → `.input-button-icon[data-slot="left"]`). The bare names
92
+ collided with consumer CSS (pitfall #16). `--input-button-text-white-space` now defaults to
93
+ `normal` (was `nowrap`, which overflowed narrow columns). `aria-disabled` is omitted instead of
94
+ `"false"` when not readonly. Added the `StressTest` story.
@@ -163,3 +163,5 @@ line-clamp tokens: an option label names the choice and must stay readable. The
163
163
  - `InputCheckboxRadioButton`'s `id` became optional with a `useId()` fallback.
164
164
  - Specs now unmount every wrapper in `afterEach`; stories moved to `StoryObj` with Docs
165
165
  descriptions, parent-story links, `CanvasSwitcher` (Field, Button) and `StressTest` stories.
166
+
167
+ **2026-10-17**: resting shadow ring, the box (`--input-checkbox-shadow`, not inside the button style) and the button style (`--input-checkbox-button-shadow`). Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -100,3 +100,8 @@ look, zero the slot margins and space it with the root margins (example in CONSU
100
100
  bottom margin collapsed through and won anyway).
101
101
  - An empty `descriptionId` no longer renders `id=""`. `styleClassPassthrough` now reacts to prop
102
102
  changes.
103
+
104
+ **2026-10-17** (stress-test pass): `--input-description-line-height` defaults to `1.5` (was
105
+ `var(--step-4)`, equal to the font size, so wrapped lines touched); the root wraps unbroken copy
106
+ (`overflow-wrap: anywhere`). Added the `StressTest` story, a CanvasSwitcher decorator, Docs page
107
+ descriptions and a link to the InputTextField story; the spec restores mocks after each test.
@@ -24,7 +24,7 @@ the surrounding theme.
24
24
  | Prop (template form) | Type | Default | Notes |
25
25
  |------|------|---------|-------|
26
26
  | `:id` | `string` | (required) | Must match an id in the control's `aria-describedby` while in error. Wrappers get it from `useAriaDescribedById(...).errorId`, which only adds it to `aria-describedby` when `fieldHasError` is true. |
27
- | `:error-message` | `string \| string[] \| object` | (required) | A string renders as one message; an array renders as a `<ul>` list. |
27
+ | `:error-message` | `string \| string[] \| object` | (required) | A string renders as one message; an array renders as a `<ul role="list">`, with blank entries dropped. Long and unbroken text wraps inside the strip (`overflow-wrap: anywhere`); it is never line-clamped. |
28
28
  | `:show-error` | `boolean` | (required) | Opens the strip. Rendered as `data-visible`; when `false` the root gets `aria-hidden="true"`. |
29
29
  | `:is-detached` | `boolean` | (required) | `true`: sits below the control with a `2rem` gap, fully rounded. `false`: tucked under the control's bottom edge. Rendered as `data-detached`. |
30
30
  | `:input-variant` | `"normal" \| "outlined" \| "underlined"` | `"normal"` | Match the control's variant. Rendered as `data-input-variant`. `underlined` drops the rounded corners and (when attached) the outline. |
@@ -41,7 +41,7 @@ No slots.
41
41
  ```vue
42
42
  <template>
43
43
  <div class="my-field">
44
- <input :id :aria-describedby="ariaDescribedby()" :aria-invalid="fieldHasError" />
44
+ <input :id :aria-describedby="ariaDescribedby()" :aria-invalid="fieldHasError ? 'true' : undefined" />
45
45
  <InputError :id="errorId" :error-message :show-error="fieldHasError" :is-detached="false" :input-variant />
46
46
  </div>
47
47
  </template>
@@ -66,6 +66,8 @@ const { id, errorId, ariaDescribedby } = useAriaDescribedById(props.name, toRef(
66
66
  read when the control is focused. Moving focus to the first invalid field on submit is the
67
67
  form's job (see `FormWrapper` / pattern forms).
68
68
  - Hidden errors are `aria-hidden="true"`; the icon is always `aria-hidden`.
69
+ - The message list keeps `role="list"`, since `list-style: none` makes Safari/VoiceOver drop list
70
+ semantics.
69
71
  - The open/close transition is disabled under `prefers-reduced-motion: reduce`.
70
72
 
71
73
  ---
@@ -103,3 +105,8 @@ re-declares those on the element itself, so ancestor overrides of them never lan
103
105
  Hardcoded font size/weight, padding, list gap and detached gap promoted to public tokens.
104
106
  - `aria-hidden` now omitted when visible (was `"false"`); icon marked `aria-hidden`;
105
107
  `prefers-reduced-motion` respected; `styleClassPassthrough` now reacts to prop changes.
108
+
109
+ **2026-10-17** (stress-test pass): added the `StressTest` story and a CanvasSwitcher decorator;
110
+ message text wraps long unbroken strings (`min-inline-size: 0`, `overflow-wrap: anywhere`) and the
111
+ icon no longer shrinks; blank array entries are dropped; the list gets `role="list"`; child rules
112
+ nested inside the root block. No line-clamp token on purpose: a truncated error can't be acted on.
@@ -41,7 +41,9 @@ Use `InputLabel` directly only when building a custom field wrapper:
41
41
  | `htmlLabel` | Rich markup (e.g. a required marker). Rendered first. Phrasing content only: it's inside a `<label>`, so no interactive elements. |
42
42
  | `textLabel` | Plain label text. |
43
43
 
44
- Both render directly inside the `<label>`, no wrapper elements. Don't hardcode a required
44
+ Both render directly inside the `<label>`, no wrapper elements. Long or unbroken copy wraps
45
+ (`overflow-wrap: anywhere` on the label); there is no line-clamp token, since slot copy is the
46
+ consumer's own markup to clamp. Don't hardcode a required
45
47
  asterisk in a slot: use the indicator below.
46
48
 
47
49
  ---
@@ -62,7 +64,7 @@ export default defineAppConfig({
62
64
  without it; `"none"`: nothing (default, so existing sites are unchanged).
63
65
  - `InputTextField`, `InputTextAsNumberField`, `InputNumberField`, `InputSelectField`,
64
66
  `InputTextareaField` and `InputRangeField` forward `:required`.
65
- `ToggleSwitchField` pass `indicator="none"`: a switch always
67
+ `ToggleSwitchField` passes `indicator="none"`: a switch always
66
68
  has a value, so neither marker means anything there.
67
69
  - The wrappers don't expose the indicator props per field; use `app.config`.
68
70
  - Text props are the i18n hook: pass translated `optionalText`/`requiredText` (or set them in
@@ -116,3 +118,7 @@ Hooks: `data-input-variant`, `data-invalid`, `.input-label-indicator[data-indica
116
118
  **2026-09-27**: added the required/optional indicator (`required`, `indicator`, `requiredText`,
117
119
  `optionalText`, `requiredIcon`, `optionalIcon`, all app.config-backed under `srcdev.inputLabel`),
118
120
  with wrappers forwarding `:required`.
121
+
122
+ **2026-10-17** (stress-test pass): `overflow-wrap: anywhere` on the label so unbroken copy and
123
+ markers wrap in a narrow column; added the `StressTest` story, a CanvasSwitcher decorator, Docs
124
+ page descriptions and a link to the InputTextField story; spec now unmounts every wrapper.
@@ -252,3 +252,5 @@ classes are gone.
252
252
  - Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
253
253
  `DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
254
254
  control's step-button demo uses `InputButton`, not raw buttons.
255
+
256
+ **2026-10-17**: resting shadow ring (`--input-number-shadow`), `normal` variant; underlined unchanged. Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -118,3 +118,5 @@ focused the first box on mount; pass `autofocus` to keep that. Its boxes were na
118
118
  read the code from `v-model` or the hidden input (`name`) instead. Rename the app's file away (or
119
119
  delete it) once on the layer release that ships this, or the app's own `InputOtp` shadows the
120
120
  layer's.
121
+
122
+ **2026-10-17**: resting shadow ring (`--input-otp-shadow`), each box, `normal` variant. Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -175,3 +175,5 @@ automatically in `<script setup>`, no destructuring or `props.` reference needed
175
175
  - Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
176
176
  `DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
177
177
  control's step-button demo uses `InputButton`, not raw buttons.
178
+
179
+ **2026-10-17**: deliberately left out of the form-control shadow ring and border-colour hover. The default mode is the native slider (no border to ring), and markers mode draws each marker's edge as an outline. Decided to keep it as it is.
@@ -151,3 +151,5 @@ the `theme` prop says; the Field root's own `data-theme` keeps the prop value.
151
151
  ```
152
152
 
153
153
  `v-model` and `v-model:field-data` are both **required**, same as `InputSelect` above.
154
+
155
+ **2026-10-17**: resting shadow ring (`--input-select-shadow`), `normal` variant; underlined keeps its old 2px hover outline. Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -218,3 +218,5 @@ A compact numeric stepper on a text input (no native spinner). Root
218
218
  - The password and stepper buttons get accessible names through `InputButton`'s `buttonText`
219
219
  (visually hidden in icon-only mode).
220
220
  - Use `autocomplete` on personal-data fields (WCAG 1.3.5).
221
+
222
+ **2026-10-17**: resting shadow ring (`--input-text-shadow`), `normal` variant; underlined unchanged. Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -145,3 +145,5 @@ with `data-theme`, `data-input-variant`, `data-invalid`, `data-dirty` and `data-
145
145
  <template #descriptionText>Please describe your enquiry in detail</template>
146
146
  </InputTextareaField>
147
147
  ```
148
+
149
+ **2026-10-17**: resting shadow ring (`--input-textarea-shadow`), `normal` variant; underlined unchanged. Theme step 2 by default, so it turns red under `data-invalid`; width is the global `--form-element-shadow-width`. Hover now draws a 1px outline in the border colour with no offset, over the ring's inner edge (was a 2px outline at a 2px offset in the focus colour); focus is unchanged and sits just outside the ring.
@@ -4,8 +4,9 @@
4
4
 
5
5
  `PendingEffect` is a decorative SVG that draws an animated dashed line around a busy button. It
6
6
  is an internal part of `InputButton`: set `has-pending-effect` on the button and toggle
7
- `is-pending`. Don't render it on its own. Its CSS depends on the host having `.pending-effect`
8
- (and `position: relative`, which that rule provides), plus `.is-pending` to show and animate it.
7
+ `is-pending`. Don't render it on its own. Its CSS depends on the host being `.input-button[data-pending-effect]`
8
+ (and `position: relative`, which that rule provides), plus `[data-pending]` to show and animate it.
9
+ Those were the classes `.pending-effect` and `.is-pending` until 2026-10-17.
9
10
 
10
11
  ```vue
11
12
  <InputButton button-text="Submit" type="submit" has-pending-effect :is-pending="submitting" />
@@ -26,10 +27,10 @@ No slots, no events.
26
27
 
27
28
  ## Behaviour
28
29
 
29
- - Hidden (`opacity: 0`) until the host has `.is-pending`.
30
+ - Hidden (`opacity: 0`) until the host has `[data-pending]`.
30
31
  - The host must not clip overflow: the line is drawn outside the host's edge. `InputButton`
31
32
  sets `overflow: hidden` on buttons generally, and switches it to `visible` when
32
- `.pending-effect` is present. Before 2026-09-26 it didn't, so the effect never rendered at all.
33
+ `[data-pending-effect]` is present. Before 2026-09-26 it didn't, so the effect never rendered at all.
33
34
  - Under `prefers-reduced-motion: reduce` it never shows, since the animation is the whole effect.
34
35
  - `aria-hidden="true"` and `focusable="false"`: it is decorative. Busy state for assistive tech
35
36
  is the host button's responsibility.