srcdev-nuxt-components 9.4.9 → 9.4.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 (178) hide show
  1. package/.claude/commands/migrate-component.md +38 -6
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/build.mjs +26 -0
  4. package/.claude/component-ledger/output.html +8 -3
  5. package/.claude/component-ledger/template.html +7 -2
  6. package/.claude/skills/component-dynamic-slots.md +1 -1
  7. package/.claude/skills/components/alert-mask-core.md +84 -0
  8. package/.claude/skills/components/canvas-switcher.md +1 -1
  9. package/.claude/skills/components/carousel-flip.md +112 -168
  10. package/.claude/skills/components/container-glow.md +123 -0
  11. package/.claude/skills/components/dashboard-quad-grid.md +98 -0
  12. package/.claude/skills/components/dashboard-stats-grid.md +103 -0
  13. package/.claude/skills/components/display-theme-switch.md +60 -0
  14. package/.claude/skills/components/glowing-border.md +66 -0
  15. package/.claude/skills/components/input-number-core.md +214 -0
  16. package/.claude/skills/components/input-range-core.md +142 -0
  17. package/.claude/skills/components/input-select-core.md +151 -0
  18. package/.claude/skills/components/input-text-core.md +16 -0
  19. package/.claude/skills/components/input-textarea-core.md +154 -0
  20. package/.claude/skills/components/section-parallax.md +32 -10
  21. package/.claude/skills/components/select-menu.md +195 -0
  22. package/.claude/skills/components/toggle-switch-core.md +150 -0
  23. package/.claude/skills/components/ui-block-decorated.md +56 -0
  24. package/.claude/skills/components/wipe-away-vertical.md +98 -0
  25. package/.claude/skills/icon-sets.md +1 -1
  26. package/.claude/skills/index.md +15 -1
  27. package/.vscode/css-custom-data.json +38 -0
  28. package/.vscode/settings.json +3 -0
  29. package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
  30. package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
  31. package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
  32. package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
  33. package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
  34. package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
  35. package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
  36. package/.vscode/srcdev-component-input-number.code-snippets +65 -0
  37. package/.vscode/srcdev-component-input-range.code-snippets +85 -0
  38. package/.vscode/srcdev-component-input-select.code-snippets +42 -0
  39. package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
  40. package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
  41. package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
  42. package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
  43. package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
  44. package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
  45. package/README.md +18 -37
  46. package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
  47. package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
  48. package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
  49. package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
  50. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
  51. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
  52. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
  53. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
  54. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
  55. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
  56. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
  57. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
  58. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
  59. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
  60. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
  61. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
  62. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
  63. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
  64. package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
  65. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
  66. package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
  67. package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
  68. package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
  69. package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
  70. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
  71. package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
  72. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
  73. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
  74. package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
  75. package/app/components/01.atoms/card/CardCore.vue +4 -4
  76. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
  77. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
  78. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
  79. package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
  80. package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
  81. package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
  82. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
  83. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
  84. package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
  85. package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
  86. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
  87. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
  88. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
  89. package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
  90. package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
  91. package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
  92. package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
  93. package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
  94. package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
  95. package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
  96. package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
  97. package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
  98. package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
  99. package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
  100. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
  101. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
  102. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
  103. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
  104. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
  105. package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
  106. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
  107. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
  108. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
  109. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
  110. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
  111. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
  112. package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
  113. package/app/components/05.forms/form-field/FormField.vue +1 -1
  114. package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
  115. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
  116. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
  117. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
  118. package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
  119. package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
  120. package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
  121. package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
  122. package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
  123. package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
  124. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
  125. package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
  126. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
  127. package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
  128. package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
  129. package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
  130. package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
  131. package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
  132. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
  133. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
  134. package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
  135. package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
  136. package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
  137. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
  138. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
  139. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
  140. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
  141. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
  142. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
  143. package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
  144. package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
  145. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
  146. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
  147. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
  148. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
  149. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
  150. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
  151. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
  152. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
  153. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
  154. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
  155. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
  156. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
  157. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
  158. package/app/types/components/container-glow.d.ts +8 -0
  159. package/app/types/components/index.ts +2 -1
  160. package/app/types/components/select-menu.d.ts +5 -0
  161. package/package.json +3 -2
  162. package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
  163. package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
  164. package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
  165. package/app/components/carousels/CarouselBasic.vue +0 -317
  166. package/app/components/carousels/CarouselInfinite.vue +0 -358
  167. package/app/components/container-glow/ContainerGlowCore.vue +0 -296
  168. package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
  169. package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
  170. package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
  171. package/app/components/glowing-border/GlowingBorder.vue +0 -142
  172. package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
  173. package/app/components/layout-grids/LayoutGridB.vue +0 -134
  174. package/app/components/test-storybook/TestStorybook.vue +0 -49
  175. package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
  176. package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
  177. package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
  178. package/app/types/components/carousel-basic.d.ts +0 -19
@@ -0,0 +1,142 @@
1
+ # InputRangeCore Component
2
+
3
+ ## Overview
4
+
5
+ `InputRangeCore` is the low-level native `<input type="range">` primitive for the `05.forms`
6
+ range-slider family. It renders a single native range input with left/right button slots (for
7
+ step-down/step-up controls), an optional `markers` slot for custom tick marks overlaid on the
8
+ track, and an optional `datalist` slot for native tick-mark snapping. It has no label,
9
+ description, or error-message rendering of its own — that's composed by the `InputRangeDefault`
10
+ variant below.
11
+
12
+ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCore` directly.
13
+
14
+ ---
15
+
16
+ ## Props reference
17
+
18
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
19
+
20
+ | Prop (template form) | Type | Default | Notes |
21
+ |------|------|---------|-------|
22
+ | `:id` | `string` | (required) | Applied to the native input. |
23
+ | `:name` | `string` | (required) | Applied to the native input, and used to derive the `list` id when the `datalist` slot is used (`${name}-datalist`). |
24
+ | `:min` | `number` | (required) | Native `min`. |
25
+ | `:max` | `number` | (required) | Native `max`. |
26
+ | `:step` | `number` | `1` | Native `step`. |
27
+ | `:required` | `boolean` | `false` | Native `required`. |
28
+ | `:placeholder` | `string` | `""` | Declared but not rendered — range inputs have no placeholder concept. Kept for prop-shape consistency with sibling `05.forms` components. |
29
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
30
+ | `:weight` | `FormWeight` | `"normal"` | Adds an `input-range--{weight}` class to the input. **No CSS in the library currently styles this class — it's a no-op today**, kept for prop-shape consistency with sibling components. |
31
+ | `:field-has-error` | `boolean` | `false` | Declared, but doesn't drive any visible state inside `InputRangeCore` itself — `InputRangeDefault` handles the actual error styling (`data-invalid`/`.error` class) on its own wrapper. |
32
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
33
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
34
+
35
+ ### v-model
36
+
37
+ `v-model` — `number`, **required** (`defineModel<number>({ required: true })`). Bound directly to
38
+ the native range input. Required rather than defaulted deliberately — a real `<input type="range">`
39
+ always has a concrete value (there's no meaningful "empty" range slider), so a default here would
40
+ be an artificial value rather than reflecting actual native behaviour. This also satisfies
41
+ `vue/require-default-prop`, which otherwise flags any `defineModel()` without a default — see
42
+ `project_ledger_eslint_issues_column` in memory for why that's the correct fix (not a rule
43
+ disable, not a fake default).
44
+
45
+ ---
46
+
47
+ ## Slots
48
+
49
+ | Slot | Notes |
50
+ |------|-------|
51
+ | `left` | Rendered before the track (e.g. a step-down button). |
52
+ | `right` | Rendered after the track (e.g. a step-up button). |
53
+ | `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Adds a `has-markers` class to the input, which shrinks its visible track to a thin `0.2rem` strip and hides the native thumb, so your marker content becomes the primary visual. Expected shape: a `.input-range-markers` wrapper containing `.marker` elements (each optionally containing a `.marker-icon`) — see the component's own scoped CSS for the classes it pre-styles. |
54
+ | `datalist` | Rendered after the input. Must contain a real `<datalist>` element whose `id` matches `${name}-datalist` for the browser to link it via the input's `list` attribute — the component computes that id string for the `list` attribute itself, but does not set the `id` on your slotted `<datalist>` for you. |
55
+
56
+ ---
57
+
58
+ ## CSS token API
59
+
60
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-range/CONSUMER-STYLING.md) for the
61
+ full table with defaults.
62
+
63
+ **Common tokens:**
64
+ - `--input-range-accent-colour` — native `accent-color` (track/thumb tint), falls back to `--theme-accent`
65
+ - `--input-range-marker-background-colour` / `-outline-colour` — marker dot styling
66
+ - `--input-range-marker-icon-colour` — falls back to `--input-range-accent-colour`
67
+
68
+ Thumb/track height and the button-slot's button size come from the shared global geometry token
69
+ `--input-range-button-size` (declared in `03.generic-input-geometry.css`, not owned by this
70
+ component), matching the convention `theming-form-geometry-tokens.md` documents for the rest of
71
+ `05.forms`.
72
+
73
+ ---
74
+
75
+ ## Fixed 2026-09-21: dead `accent-color` token
76
+
77
+ `accent-color` previously read `--theme-form-range-accent-color`, a custom property that was never
78
+ declared anywhere in the codebase, with no fallback. Per the CSS spec, an undefined `var()` with
79
+ no fallback makes the whole declaration invalid at computed-value time — so `accent-color` was
80
+ silently falling back to the browser's native default (an OS-dependent blue) instead of the
81
+ intended theme colour, in every place it was used (the input itself, its `.has-markers` state, and
82
+ the marker icon colour). This is the same bug class found in `CarouselFlip`'s edge-preview tokens
83
+ around the same time — check any component reaching for a `--theme-*`-prefixed custom property for
84
+ a real declaration of that name before trusting it renders correctly.
85
+
86
+ Also removed: a broken `::-webkit-slider-thumb` rule (`background-color: 0.1rem solid green` is
87
+ invalid CSS and was silently dropped; `accent-color`/`color` don't apply to that pseudo-element at
88
+ all) and ~60 lines of dead commented-out alternative thumb/track styling — none of it had any
89
+ visible effect, since `appearance: none` (required for custom thumb/track styling to take effect)
90
+ was itself commented out.
91
+
92
+ ---
93
+
94
+ ## Variants
95
+
96
+ ### InputRangeDefault
97
+
98
+ `InputRangeDefault` (`variants/InputRangeDefault.vue`) composes `InputRangeCore` with `InputLabel`,
99
+ `InputDescription`, and `InputError`, plus optional step-down/step-up buttons (`InputButtonCore`)
100
+ wired into the `left`/`right` slots.
101
+
102
+ **Additional props over InputRangeCore:**
103
+
104
+ | Prop (template form) | Type | Default | Notes |
105
+ |------|------|---------|-------|
106
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
107
+ | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
108
+ | `:step-down-label` | `string` | `"Step down"` | `button-text` on the step-down `InputButtonCore` — override for localisation. |
109
+ | `:step-up-label` | `string` | `"Step up"` | `button-text` on the step-up `InputButtonCore` — override for localisation. |
110
+
111
+ **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
112
+ `left`/`right`/`markers`/`datalist` (forwarded straight through to the underlying
113
+ `InputRangeCore` — the step-down/step-up buttons only render when you use the `left`/`right`
114
+ slots yourself, since the button icon content itself is consumer-supplied via `#iconOnly`).
115
+
116
+ ```vue
117
+ <InputRangeDefault
118
+ name="priceRange"
119
+ label="Price range"
120
+ v-model="price"
121
+ :min="0"
122
+ :max="500"
123
+ :step="10"
124
+ error-message="Please choose a value within the allowed range"
125
+ :field-has-error="hasError"
126
+ >
127
+ <template #left><Icon name="mdi:minus" /></template>
128
+ <template #right><Icon name="mdi:plus" /></template>
129
+ </InputRangeDefault>
130
+ ```
131
+
132
+ `v-model` is `number`, **required** — same as `InputRangeCore` above.
133
+
134
+ **Fixed 2026-09-21**: removed a dead `deepCssClassPassthrough` prop (declared, never consumed
135
+ anywhere in the component or referenced elsewhere in the codebase) and a dead `<style>` block
136
+ targeting `.input-range-label`/`.label-description` — classes that nothing in this component (or
137
+ `InputLabel`/`InputDescription`) actually renders, so the block had no effect. Also removed two
138
+ dead commented-out lines (`// const id = useId();`, a duplicate no-op `FormUiTheme` computed), and
139
+ (same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRangeCore` and
140
+ `InputRangeDefault` that had become entirely unused once the dead `FormUiTheme` computed was
141
+ removed — the template's `:id`/`:name`/etc. shorthand bindings resolve straight from `defineProps`
142
+ automatically in `<script setup>`, no destructuring or `props.` reference needed.
@@ -0,0 +1,151 @@
1
+ # InputSelectCore Component
2
+
3
+ ## Overview
4
+
5
+ `InputSelectCore` is the low-level native `<select>` primitive for the `05.forms` select family.
6
+ It renders a native select with a data-driven list of `<option>`s (each optionally decorated with
7
+ an icon), an optional placeholder option, and adopts the modern CSS Customizable Select API
8
+ (`appearance: base-select`) for a fully styleable open-picker state where supported, with a
9
+ graceful CSS fallback (see "Browser support" below). It has no label, description, or
10
+ error-message rendering of its own — that's composed by the `InputSelectWithLabel` variant below.
11
+
12
+ Most consumers should reach for **InputSelectWithLabel** rather than `InputSelectCore` directly.
13
+
14
+ ---
15
+
16
+ ## Props reference
17
+
18
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
19
+
20
+ | Prop (template form) | Type | Default | Notes |
21
+ |------|------|---------|-------|
22
+ | `:id` | `string` | (required) | Applied to the native select. |
23
+ | `:name` | `string` | (required) | Applied to the native select. |
24
+ | `:placeholder` | `string` | `""` | Renders a disabled placeholder `<option>` (selected when `modelValue` is empty). No option rendered at all when unset. |
25
+ | `:title` | `string` | `"Please select an option"` | Native `title` attribute (tooltip). |
26
+ | `:required` | `boolean` | `false` | Native `required`. |
27
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
28
+ | `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
29
+ | `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper and `aria-invalid` on the select. |
30
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the select element. |
31
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native select. |
32
+
33
+ ### v-models
34
+
35
+ - `v-model` — `string | number | readonly number[]`, **required** (`defineModel({ required: true })`, satisfying `vue/require-default-prop` — see `project_ledger_eslint_issues_column` in memory).
36
+ - `v-model:field-data` — `IFormMultipleOptions` (`{ data: IOptionsConfig[]; total; skip; limit }`), **required**. The `data` array drives the rendered `<option>`s. The template dereferences `fieldData.data` unconditionally, so a consumer that doesn't bind `v-model:field-data` will crash on render — `required: true` documents that constraint (it's a dev-time warning only, not a runtime guard).
37
+ - `v-model:is-dirty` — `boolean`. Set to `true` on the native `change` event (never reset to `false` by this component).
38
+ - `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`), toggling `.active` on the wrapper.
39
+
40
+ ---
41
+
42
+ ## CSS token API
43
+
44
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-select/CONSUMER-STYLING.md) for the
45
+ full table with defaults.
46
+
47
+ **Common tokens:**
48
+ - `--input-select-surface` / `-surface-hover` — wrapper/select background and option-row hover background, fall back to `--theme-input-surface`/`-surface-hover`
49
+ - `--input-select-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline (also the open-picker's own border/outline), fall back to `--theme-border`/`--theme-border-focus`
50
+
51
+ ---
52
+
53
+ ## Browser support: `appearance: base-select`
54
+
55
+ The open-picker styling (`&::picker(select)`, `:open`/`:not(:open)` states, the transition/opacity
56
+ `@starting-style` reveal) relies on the CSS Customizable Select API
57
+ (`appearance: base-select`/`::picker(select)`), a very new feature not yet supported everywhere.
58
+ The component degrades gracefully in non-supporting browsers rather than breaking: `appearance:
59
+ none` is set first (unconditionally), then overridden by `appearance: base-select` only where the
60
+ browser recognizes that value — an unsupported value is simply ignored by the CSS parser, so the
61
+ select falls back to the browser's native (unstyled) dropdown instead of losing functionality.
62
+ Per pitfall #19's general lesson (checking WebKit support specifically for load-bearing use of a
63
+ very new CSS feature, since Chrome's device-emulation mode can't catch a WebKit-only gap): this
64
+ fallback is CSS-only degradation, not a functional break like `ExpandingPanel`'s `::details-content`
65
+ case was, so it's lower risk — but the open-picker's custom border/outline/transition styling
66
+ should still be spot-checked on a real Safari session before treating it as visually final.
67
+
68
+ ---
69
+
70
+ ## Fixed 2026-09-22: dead functionality and prop-shape bugs
71
+
72
+ `InputSelectCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
73
+ score-2/5 state (no tests, no skill doc, no snippet).
74
+
75
+ - **`required` prop was declared but never bound to the native `<select>`** — native HTML
76
+ `required` validation was completely inert regardless of the prop's value. Fixed by adding
77
+ `:required` to the select.
78
+ - **`styleClassPassthrough` prop was declared but never consumed** — no `useStyleClassPassthrough`
79
+ call existed anywhere in the script, and the select's `class` was a hardcoded static string.
80
+ The prop was entirely dead; a consumer passing it had no effect. Fixed by wiring up
81
+ `useStyleClassPassthrough` and binding `elementClasses` alongside the static class.
82
+ - **`isDirty`/`isActive` models were declared and read in the wrapper's CSS class bindings
83
+ (`{ dirty: isDirty }`, `{ active: isActive }`), but nothing ever set them** — no `focusin`/
84
+ `focusout`/`change` handlers existed at all, unlike `InputTextareaCore`'s `updateFocus`
85
+ pattern. The `.dirty`/`.active` CSS states could never trigger. Fixed by adding
86
+ `@focusin`/`@focusout` (toggling `isActive`) and `@change` (setting `isDirty = true`) directly
87
+ on the select.
88
+ - **The placeholder `<option>` used `readonly`**, which is not a valid/supported HTML attribute
89
+ on `<option>` — a silent no-op, leaving the placeholder fully selectable as a real answer
90
+ (defeating its purpose). Fixed by switching to `disabled`, the correct native pattern for a
91
+ non-selectable placeholder option.
92
+ - **`theme`/`inputVariant` were typed as inline unions** (`"default" | "success" | "error" |
93
+ "warning"` / `"normal" | "outlined" | "underlined"`) instead of importing the shared
94
+ `FormUiTheme`/`InputUiVariant` types — aligned with the rest of `05.forms`.
95
+ - **`fieldData`'s `defineModel` used an untyped `defineModel("fieldData") as Ref<T>` cast**
96
+ instead of the proper generic `defineModel<T>("fieldData")` call, and had no default — the
97
+ `as` cast also hid the prop from ESLint's `vue/require-default-prop` detection entirely (it
98
+ didn't fire on `InputSelectCore.vue`, only on `InputSelectWithLabel.vue`'s correctly-typed
99
+ equivalent). Fixed by using the proper generic form with `{ required: true }` on both files —
100
+ see the `v-model:field-data` note above for why `required` is the meaningful fix here (the
101
+ template would otherwise crash on `fieldData.data` with no data bound).
102
+ - **Dead commented-out CSS** (~8 lines, a "legacy support - eg, Safari" `::after` arrow
103
+ fallback that was never active) and a **redundant duplicate declaration**
104
+ (`&.underlined { background-color: var(--_surface); }`, re-setting a value the unscoped base
105
+ rule already applied unconditionally) were removed.
106
+ - **CSS token cleanup**: `--_surface`, `--_surface-hover`, `--_border-hover`, and
107
+ `--_outline-color` were each used at exactly one point with no composition or state-swap
108
+ behind them (`--_outline-color` was a bare alias for `--_border-focus`, per pitfall #20) — all
109
+ four inlined directly to their public `var(--input-select-*, {default})` form at their point of
110
+ use. `--_border` (reused across `.normal`/`.underlined`/open-picker) and `--_border-focus`
111
+ (used directly once, plus now the open-picker's outline too) correctly stayed private.
112
+
113
+ ---
114
+
115
+ ## Variants
116
+
117
+ ### InputSelectWithLabel
118
+
119
+ `InputSelectWithLabel` (`variants/InputSelectWithLabel.vue`) composes `InputSelectCore` with
120
+ `InputLabel`, `InputDescription` (positioned before or after the field depending on
121
+ `inputVariant === "outlined"`, matching `InputTextWithLabel`'s exact placement rule), and
122
+ `InputError`.
123
+
124
+ **Additional props over InputSelectCore:**
125
+
126
+ | Prop (template form) | Type | Default | Notes |
127
+ |------|------|---------|-------|
128
+ | `:data-testid` | `string` | `"input-select-with-label"` | Sets `data-testid` on the wrapper — used for the wrapper element specifically (`:data-testid` shorthand resolves to the camelCase `dataTestid` prop, same convention as `:aria-describedby`). |
129
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
130
+ | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
131
+
132
+ **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`).
133
+
134
+ ```vue
135
+ <InputSelectWithLabel
136
+ name="colour"
137
+ label="Favourite colour"
138
+ v-model="colour"
139
+ v-model:field-data="colourOptions"
140
+ placeholder="Choose a colour"
141
+ :error-message="errors.colour"
142
+ :field-has-error="!!errors.colour"
143
+ >
144
+ <template #descriptionText>Pick the colour you like best</template>
145
+ </InputSelectWithLabel>
146
+ ```
147
+
148
+ `v-model` and `v-model:field-data` are both **required** — same as `InputSelectCore` above. Note
149
+ this wrapper passes `:theme="FormUiTheme"` (a local `computed`, not the raw `theme` prop) down to
150
+ `InputSelectCore`, so the theme automatically switches to `"error"` whenever `fieldHasError` is
151
+ true, regardless of the `theme` prop's own value.
@@ -163,6 +163,22 @@ Extra props: `min: number` (required), `max: number` (required), `step?: number`
163
163
  `stepDownText`/`stepUpText` (default `"Step down"`/`"Step up"`) — the decrement/increment buttons'
164
164
  accessible names. Requires `left`/`right` slot content for the step icons (no default icon).
165
165
 
166
+ **Fixed 2026-09-22**: `updateElementClasses(["input-text-as-number", "has-left-button",
167
+ "has-right-button"])` computes marker classes onto this component's own `elementClasses`, which is
168
+ bound to the outer `.input-text-with-label` wrapper — but the nested `<InputTextCore>` was passed
169
+ `:style-class-passthrough` as a bare shorthand, which resolves to the raw incoming
170
+ `styleClassPassthrough` **prop**, not the local `elementClasses`. So the marker classes never
171
+ reached the actual `<input>`, the scoped `.input-text-core.input-text-as-number { width:
172
+ fit-content; text-align: center; ... }` CSS never matched anything, and the input silently fell
173
+ back to `InputTextCore`'s default full-width, left-aligned styling instead of the intended
174
+ compact, centered numeric stepper box. Fixed by changing that binding to
175
+ `:style-class-passthrough="elementClasses"` — `elementClasses` still carries any consumer-supplied
176
+ `styleClassPassthrough` too, since the composable seeds it from the prop before the marker classes
177
+ are toggled on top. When a component computes its own marker/state classes via
178
+ `updateElementClasses()` and needs them on a *child* component rather than its own root element,
179
+ always pass the computed `elementClasses` explicitly — the `:x` shorthand only ever resolves to
180
+ the prop of that exact name, never a same-named local composable output.
181
+
166
182
  ---
167
183
 
168
184
  ## Accessibility
@@ -0,0 +1,154 @@
1
+ # InputTextareaCore Component
2
+
3
+ ## Overview
4
+
5
+ `InputTextareaCore` is the low-level native `<textarea>` primitive for the `05.forms` multi-line
6
+ text family. It renders a single native textarea with left/right decorative slots. It has no
7
+ label, description, or error-message rendering of its own — that's composed by the
8
+ `InputTextareaWithLabel` variant below.
9
+
10
+ Most consumers should reach for **InputTextareaWithLabel** rather than `InputTextareaCore`
11
+ directly.
12
+
13
+ ---
14
+
15
+ ## Props reference
16
+
17
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:field-has-error`, `:style-class-passthrough`, `:aria-describedby`.
18
+
19
+ | Prop (template form) | Type | Default | Notes |
20
+ |------|------|---------|-------|
21
+ | `:id` | `string` | (required) | Applied to the native textarea. |
22
+ | `:name` | `string` | (required) | Applied to the native textarea. |
23
+ | `:maxlength` | `number` | `255` | Native `maxlength`. |
24
+ | `:placeholder` | `string` | `""` | Native `placeholder`. |
25
+ | `:required` | `boolean` | `false` | Native `required`. |
26
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
27
+ | `:input-variant` | `InputUiVariant` | `"normal"` | Adds the variant name as a class on the wrapper — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
28
+ | `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper and `aria-invalid` on the textarea. |
29
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the textarea element. |
30
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native textarea. |
31
+
32
+ ### v-models
33
+
34
+ - `v-model` — `string | number | readonly string[] | null | undefined`, **required**
35
+ (`defineModel({ required: true })`, satisfying `vue/require-default-prop` the same way as
36
+ `InputRangeCore`/`InputNumberCore` — see `project_ledger_eslint_issues_column` in memory).
37
+ "Required" here means a consumer must bind `v-model`, not that the value can't be an empty
38
+ string — `""` is a perfectly valid textarea value.
39
+ - `v-model:is-dirty` — `boolean`. Set once by `InputTextareaWithLabel` the first time the value
40
+ becomes non-empty (never reset back to `false` by this component).
41
+ - `v-model:is-active` — `boolean`. Tracks native focus/blur (`focusin`/`focusout`), toggling
42
+ `.active` on the wrapper and textarea.
43
+
44
+ ---
45
+
46
+ ## Slots
47
+
48
+ | Slot | Notes |
49
+ |------|-------|
50
+ | `left` | Rendered before the textarea (decorative — e.g. an icon). Adds `has-left-slot` to the wrapper. |
51
+ | `right` | Rendered after the textarea (decorative). Adds `has-right-slot` to the wrapper. |
52
+
53
+ Unlike `InputNumberCore`/`InputRangeCore`, these slots have no built-in `InputButtonCore`
54
+ divider/embedded-button styling — they're plain decorative content (see the Storybook stories,
55
+ which use emoji), not a stepper control.
56
+
57
+ ---
58
+
59
+ ## CSS token API
60
+
61
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-textarea/CONSUMER-STYLING.md) for
62
+ the full table with defaults.
63
+
64
+ **Common tokens:**
65
+ - `--input-textarea-surface` — wrapper background, falls back to `--theme-input-surface`
66
+ - `--input-textarea-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline, fall back to `--theme-border`/`--theme-border-focus`
67
+ - `--input-textarea-text-color` — textarea text colour, falls back to `--theme-input-text-color-normal`
68
+ - `--input-textarea-placeholder-color` — placeholder text colour, falls back to `--theme-input-placeholder`
69
+
70
+ ---
71
+
72
+ ## Fixed 2026-09-22: undefined tokens and dead code
73
+
74
+ `InputTextareaCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
75
+ score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill doc, no snippet).
76
+
77
+ - **Silently broken styling from undefined CSS custom properties** (same bug class as pitfall #21
78
+ and `InputRangeCore`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
79
+ read `--element-decorator-padding-inline`, and the placeholder's `font-size`/`font-style`/
80
+ `line-height`/`font-weight` read `--input-placeholder-font-size`/`-font-style`/`-line-height`/
81
+ `-font-weight` — **none of these five custom properties were declared anywhere in the
82
+ codebase**. Per the CSS spec, an undefined `var()` with no fallback makes the whole declaration
83
+ invalid, so all five silently fell back to the browser/inherited default instead of the intended
84
+ design. Fixed by switching to the tokens `InputTextCore` actually uses for the same purpose:
85
+ `--input-padding-inline` for the wrapper padding, and `--theme-input-placeholder-font-size` +
86
+ hardcoded `italic`/`1`/`normal` for the placeholder (matching `InputTextCore`'s placeholder rule
87
+ exactly — those three placeholder sub-properties aren't tokenized there either).
88
+ - **Zero public `--input-textarea-*` tokens existed at all** — every colour value read a
89
+ `--theme-*` global directly, with no component-level override point (the exact gap pitfall #14
90
+ describes for early `05.forms` components). Added the full set matching `InputTextCore`'s shape:
91
+ `--input-textarea-surface`, `-border`, `-border-hover`, `-border-focus`, `-text-color`,
92
+ `-placeholder-color`.
93
+ - **Dead code**: both `InputTextareaCore` and `InputTextareaWithLabel` declared
94
+ `const FormUiTheme = computed(() => props.fieldHasError ? "error" : props.theme);` — a value
95
+ binding that shadows the imported `FormUiTheme` *type* import of the same name, and is never
96
+ referenced anywhere in either file (the template's `:theme` shorthand resolves straight from
97
+ `defineProps`). Removed both. `InputTextareaWithLabel` also had ~5 lines of commented-out dead
98
+ code (an old, hand-rolled `id`/`errorId`/`ariaDescribedby` implementation, superseded by
99
+ `useAriaDescribedById` right below it) — removed. (The identical dead `FormUiTheme` computed
100
+ still exists in `InputTextCore`/`InputTextWithLabel` themselves — out of scope for this pass
101
+ since those weren't being touched, but worth cleaning up next time either is migrated.)
102
+ - **`InputTextareaWithLabel` passed `:label` down to `InputTextareaCore`**, which has no `label`
103
+ prop (matching `InputTextCore`, which doesn't have one either) — it fell through as a
104
+ meaningless raw `label="..."` HTML attribute on the wrapper `<div>`. Removed the pass-through;
105
+ `InputLabel` (which does have a real `label` concept) already renders it correctly. Covered by
106
+ a regression test.
107
+ - **`defineModel()` had no default**, tripping `vue/require-default-prop` in both files — fixed
108
+ with `{ required: true }`, same pattern as `InputRangeCore`/`InputNumberCore` (see the v-model
109
+ note above for why this is still correct even though an empty string is a valid value here).
110
+ - **`InputTextareaWithLabel.stories.ts` used `reactive()` in two render functions
111
+ (`AllVariants`/`FormExamples`) without importing it** — only `computed` was imported from
112
+ `"vue"`. This library's `.ts` Storybook files need Vue reactivity APIs imported explicitly (no
113
+ Nuxt auto-import outside `.vue` files) — a real runtime `ReferenceError` waiting to happen the
114
+ first time either story rendered. Fixed by adding `reactive` to the existing `computed` import.
115
+
116
+ ---
117
+
118
+ ## Variants
119
+
120
+ ### InputTextareaWithLabel
121
+
122
+ `InputTextareaWithLabel` (`variants/InputTextareaWithLabel.vue`) composes `InputTextareaCore` with
123
+ `InputLabel`, `InputDescription` (positioned before or after the field depending on
124
+ `inputVariant === "outlined"`, matching `InputTextWithLabel`'s exact placement rule), and
125
+ `InputError`.
126
+
127
+ **Additional props over InputTextareaCore:**
128
+
129
+ | Prop (template form) | Type | Default | Notes |
130
+ |------|------|---------|-------|
131
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
132
+ | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
133
+
134
+ **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
135
+ `left`/`right` (forwarded straight through to the underlying `InputTextareaCore`).
136
+
137
+ ```vue
138
+ <InputTextareaWithLabel
139
+ name="message"
140
+ label="Message"
141
+ v-model="message"
142
+ placeholder="How can we help you?"
143
+ :maxlength="500"
144
+ :error-message="errors.message"
145
+ :field-has-error="!!errors.message"
146
+ >
147
+ <template #descriptionText>Please describe your inquiry in detail</template>
148
+ </InputTextareaWithLabel>
149
+ ```
150
+
151
+ `v-model` is `string | number | readonly string[] | null | undefined`, **required** — same as
152
+ `InputTextareaCore` above. `isDirty`/`isActive` are managed internally (not exposed as props) —
153
+ `isDirty` flips to `true` the first time the value becomes a non-empty string (checked on mount
154
+ and on every change), and never resets.
@@ -52,20 +52,37 @@ type: reference
52
52
  ## How it works
53
53
 
54
54
  The component sets:
55
+
55
56
  - `background-image: url(...)` via `v-bind`
56
- - `background-position: center`
57
- - `background-size: inherit` (fallback when fixed is not supported)
58
- - `min-height: 120vh`
59
- - `background-color: light-dark(var(--slate-01), var(--slate-08))` (visible if image fails to load)
57
+ - `background-position: var(--section-parallax-background-position, center)`
58
+ - `background-size: var(--section-parallax-background-size, cover)`
59
+ - `min-height: var(--section-parallax-min-height, 100svh)`
60
+ - `background-color: var(--section-parallax-background-colour, light-dark(var(--slate-01), var(--slate-08)))` (visible if image fails to load)
61
+
62
+ Inside `@media (hover: hover) and (pointer: fine) { @supports (background-attachment: fixed) { ... } }`:
60
63
 
61
- Inside `@supports (background-attachment: fixed)`:
62
64
  - `background-attachment: fixed` — pins the image to the viewport
63
- - `background-size: cover` — ensures the image fills the viewport
64
- - `min-height: 120vh` — ensures enough scroll travel to see the parallax motion
65
+ - `min-height: var(--section-parallax-min-height-fixed, 120vh)` — ensures enough scroll travel to see the parallax motion
66
+
67
+ Under `@media (prefers-reduced-motion: reduce)`, the parallax is disabled regardless of hover/
68
+ `@supports` support: `background-attachment` reverts to `scroll` and height reverts to
69
+ `--section-parallax-min-height`.
65
70
 
66
- ## Controlling height
71
+ ## Styling
72
+
73
+ Height, background position/size/colour are all public CSS custom properties — see
74
+ `CONSUMER-STYLING.md` for the full token table.
75
+
76
+ ```vue
77
+ <SectionParallax
78
+ background-image="/images/candle-and-stones.jpg"
79
+ style="--section-parallax-min-height: 60vh; --section-parallax-min-height-fixed: 70vh;"
80
+ >
81
+ <p>Shorter atmospheric break</p>
82
+ </SectionParallax>
83
+ ```
67
84
 
68
- Override `min-height` with a consuming-page style:
85
+ Alternatively, override `min-height` with a consuming-page style:
69
86
 
70
87
  ```css
71
88
  .my-page {
@@ -98,5 +115,10 @@ Override `min-height` with a consuming-page style:
98
115
  ## Notes
99
116
 
100
117
  - The `@supports` guard means the parallax activates only when the browser supports `background-attachment: fixed`. No JS is involved.
101
- - Slot content is only rendered when the `default` slot is provided (`v-if="slots.default"`).
102
118
  - The component has no built-in overlay or gradient — add one via the slot or a `::before` pseudo-element in your consuming-page styles.
119
+ - 2026-09-20 migration: moved from `app/components/parallax/` (unplaced) into
120
+ `01.atoms/animations/section-parallax/`; props pattern was already `interface Props` +
121
+ `withDefaults` (no change needed); promoted min-height/background-position/background-size/
122
+ background-colour (previously hardcoded) to public `--section-parallax-*` tokens; added a
123
+ `prefers-reduced-motion: reduce` guard that disables the fixed-attachment parallax. No behaviour
124
+ change beyond the new override surface and the reduced-motion opt-out.