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,98 @@
1
+ # DashboardQuadGrid Component
2
+
3
+ ## Overview
4
+
5
+ `DashboardQuadGrid` (renamed from `LayoutGridA`) is a fixed 4-panel dashboard layout. Unlike
6
+ `LayoutGridByCols`/`LayoutGridByWidth`, it does not take a column-count prop — it has four
7
+ **named** slots (`slot1`–`slot4`) with a fixed responsive arrangement that changes shape (not
8
+ just column count) across breakpoints, designed for dashboard-style layouts (one primary panel
9
+ plus three supporting panels).
10
+
11
+ ---
12
+
13
+ ## Slot pattern
14
+
15
+ Exactly four fixed slot names — `slot1`, `slot2`, `slot3`, `slot4`. Not dynamic; each is a named
16
+ slot with a specific position in the layout at each breakpoint.
17
+
18
+ ```vue
19
+ <DashboardQuadGrid>
20
+ <template #slot1><ArticleCard /></template>
21
+ <template #slot2><StatsPanel /></template>
22
+ <template #slot3><QuickActions /></template>
23
+ <template #slot4><ActivityFeed /></template>
24
+ </DashboardQuadGrid>
25
+ ```
26
+
27
+ ---
28
+
29
+ ## Props reference
30
+
31
+ | Prop (template form) | Type | Default | Notes |
32
+ |------|------|---------|-------|
33
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
34
+
35
+ ---
36
+
37
+ ## Responsive behaviour
38
+
39
+ Uses **CSS container queries** (`container-type: inline-size`) on the wrapper — responds to its
40
+ own container width, not the viewport.
41
+
42
+ - **Below 768px**: all four slots stack vertically in order (slot1 → slot2 → slot3 → slot4).
43
+ - **768px–1059px**: 2-column, 3-row grid. `slot1` and `slot2` each span the full width (rows 1
44
+ and 2); `slot3`/`slot4` sit side by side in row 3.
45
+ - **≥1060px**: 3-column, 2-row grid. `slot1` spans vertically down the left column; `slot2` spans
46
+ horizontally across the top-right; `slot3`/`slot4` sit in the bottom-right.
47
+
48
+ This shape is structural — there is no prop to reconfigure it. If you need an arbitrary
49
+ column count instead, use `LayoutGridByCols`/`LayoutGridByWidth`.
50
+
51
+ ---
52
+
53
+ ## CSS token API
54
+
55
+ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md).
56
+
57
+ | Token | Default | Controls |
58
+ |---|---|---|
59
+ | `--dashboard-quad-grid-gap` | `2rem` | Gap between the four slot panels |
60
+ | `--dashboard-quad-grid-padding` | `2rem` | Padding inside each slot panel |
61
+ | `--dashboard-quad-grid-outline-width` | `0.1rem` | Slot panel outline width |
62
+ | `--dashboard-quad-grid-outline-colour` | `light-dark(black, white)` | Slot panel outline colour |
63
+ | `--dashboard-quad-grid-outline-offset` | `0` | Slot panel outline offset |
64
+ | `--dashboard-quad-grid-border-width` | `0` | Slot panel border width (off by default — the panel edge is drawn by the outline) |
65
+ | `--dashboard-quad-grid-border-colour` | `light-dark(black, white)` | Slot panel border colour, only visible once `--dashboard-quad-grid-border-width` is set |
66
+ | `--dashboard-quad-grid-border-radius` | `0.5rem` | Slot panel corner radius |
67
+ | `--dashboard-quad-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
68
+ | `--dashboard-quad-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
69
+ | `--dashboard-quad-grid-rule-inset` | `0` | Inset of the grid gap rule line from the panel edges |
70
+
71
+ ---
72
+
73
+ ## Local style override scaffold
74
+
75
+ ```vue
76
+ <DashboardQuadGrid :style-class-passthrough="['my-grid']">
77
+ ...
78
+ </DashboardQuadGrid>
79
+
80
+ <style>
81
+ .dashboard-quad-grid-wrapper {
82
+ &.my-grid {
83
+ --dashboard-quad-grid-gap: 1.6rem;
84
+ }
85
+ }
86
+ </style>
87
+ ```
88
+
89
+ ---
90
+
91
+ ## Notes
92
+
93
+ - Auto-imported in Nuxt — no manual import needed.
94
+ - Renamed from `LayoutGridA` during the 2026-09-21 compliance migration; previously lived
95
+ unplaced at `app/components/layout-grids/LayoutGridA.vue` with options-style props and no
96
+ public CSS tokens (its geometry values were hardcoded px literals).
97
+ - For a generic, arbitrary-column-count grid, use `LayoutGridByCols`/`LayoutGridByWidth` instead
98
+ — this component's 4-slot shape is fixed and specifically dashboard-oriented.
@@ -0,0 +1,103 @@
1
+ # DashboardStatsGrid Component
2
+
3
+ ## Overview
4
+
5
+ `DashboardStatsGrid` (renamed from `LayoutGridB`) is a dashboard layout combining a multi-panel
6
+ top row (a cluster of small stat/summary panels plus two static side panels) with a strip of
7
+ panels in the bottom row. Item counts in the two dynamic clusters are prop-driven; the two static
8
+ top-row slots and overall shape are fixed.
9
+
10
+ ---
11
+
12
+ ## Slot pattern
13
+
14
+ - `top-row-slot1-{n}-content` — dynamic, `n` from `1` to `topRowSlot1ItemCount`
15
+ - `top-row-slot-2` — static
16
+ - `top-row-slot-3` — static
17
+ - `bottom-row-{n}-content` — dynamic, `n` from `1` to `bottomRowItemCount`
18
+
19
+ ```vue
20
+ <DashboardStatsGrid :top-row-slot1-item-count="3" :bottom-row-item-count="2">
21
+ <template #top-row-slot1-1-content><StatCard label="Uptime" value="98.5%" /></template>
22
+ <template #top-row-slot1-2-content><StatCard label="Users" value="1,247" /></template>
23
+ <template #top-row-slot1-3-content><StatCard label="Latency" value="342ms" /></template>
24
+
25
+ <template #top-row-slot-2><FeaturedPanel /></template>
26
+ <template #top-row-slot-3><InfoPanel /></template>
27
+
28
+ <template #bottom-row-1-content><FeaturePanel /></template>
29
+ <template #bottom-row-2-content><FeaturePanel /></template>
30
+ </DashboardStatsGrid>
31
+ ```
32
+
33
+ ---
34
+
35
+ ## Props reference
36
+
37
+ | Prop (template form) | Type | Default | Notes |
38
+ |------|------|---------|-------|
39
+ | `:top-row-slot1-item-count` | `number` | `6` | Number of dynamic panels rendered in the top-row cluster (`top-row-slot1-{n}-content`). |
40
+ | `:bottom-row-item-count` | `number` | `4` | Number of dynamic panels rendered in the bottom row (`bottom-row-{n}-content`). |
41
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
42
+
43
+ ---
44
+
45
+ ## Responsive behaviour
46
+
47
+ Uses **CSS container queries** (`container-type: inline-size`) — responds to its own container
48
+ width, not the viewport.
49
+
50
+ - **Top row, below 1024px**: `slot1` cluster, `slot2`, `slot3` stack vertically.
51
+ - **Top row, ≥1024px**: two-column layout — `slot1` cluster + `slot3` on the left
52
+ (`minmax(46rem, 33%)` on the right for `slot2`), with `slot2` spanning both rows on the right.
53
+ - **`slot1` cluster, below 680px**: 2 columns.
54
+ - **`slot1` cluster, ≥680px**: 3 columns.
55
+ - **Bottom row**: always a fixed 2-column grid regardless of container width.
56
+
57
+ This shape is structural — there is no prop to reconfigure it.
58
+
59
+ ---
60
+
61
+ ## CSS token API
62
+
63
+ See [CONSUMER-STYLING.md](../../app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md).
64
+
65
+ | Token | Default | Controls |
66
+ |---|---|---|
67
+ | `--dashboard-stats-grid-gap` | `1.2rem` | Gap between panels, in every row |
68
+ | `--dashboard-stats-grid-panel-padding` | `1.2rem` | Padding inside each panel |
69
+ | `--dashboard-stats-grid-border-width` | `0.1rem` | Panel border width |
70
+ | `--dashboard-stats-grid-border-colour` | `light-dark(hsl(0, 29%, 3%), hsl(0, 0%, 92%))` | Panel border colour |
71
+ | `--dashboard-stats-grid-border-radius` | `1.2rem` | Panel corner radius |
72
+ | `--dashboard-stats-grid-rule-width` | `0.1rem` | Width of the grid gap rule line (CSS Gap Decorations, limited browser support) |
73
+ | `--dashboard-stats-grid-rule-colour` | `transparent` | Colour of the grid gap rule line, invisible by default |
74
+ | `--dashboard-stats-grid-rule-inset` | `0` | Inset of the grid gap rule line from the panel edges |
75
+
76
+ ---
77
+
78
+ ## Local style override scaffold
79
+
80
+ ```vue
81
+ <DashboardStatsGrid :style-class-passthrough="['my-grid']">
82
+ ...
83
+ </DashboardStatsGrid>
84
+
85
+ <style>
86
+ .dashboard-stats-grid {
87
+ &.my-grid {
88
+ --dashboard-stats-grid-gap: 1.6rem;
89
+ }
90
+ }
91
+ </style>
92
+ ```
93
+
94
+ ---
95
+
96
+ ## Notes
97
+
98
+ - Auto-imported in Nuxt — no manual import needed.
99
+ - Renamed from `LayoutGridB` during the 2026-09-21 compliance migration; previously lived
100
+ unplaced at `app/components/layout-grids/LayoutGridB.vue` with options-style props. Its old
101
+ `--_color` custom property was dead code (declared, never consumed) and was removed rather than
102
+ promoted; `--_gap`/`--_border-color` had no public fallback and are now
103
+ `--dashboard-stats-grid-gap`/`--dashboard-stats-grid-border-colour`.
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: DisplayThemeSwitch
3
+ description: DisplayThemeSwitch system/light/dark theme picker wired to useSettingsStore, wraps TripleToggleSwitchCore
4
+ type: reference
5
+ ---
6
+
7
+ # DisplayThemeSwitch
8
+
9
+ ## Overview
10
+
11
+ `DisplayThemeSwitch` is a system/light/dark colour-scheme picker. It's a thin wrapper around
12
+ `TripleToggleSwitchCore` (`app/components/05.forms/triple-toggle-switch/`), wired to
13
+ `useSettingsStore` (`app/stores/useSettingsStore.ts`) so selecting an option updates the app's
14
+ `colourScheme` state and applies the matching class (`system`/`light`/`dark`) to
15
+ `<html>`. Renders inside `<ClientOnly>` since the store's persisted value isn't known until
16
+ hydration.
17
+
18
+ ## Props
19
+
20
+ | Prop | Type | Default | Description |
21
+ |------|------|---------|-------------|
22
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes on the root. Pass `"small"` for compact sizing. |
23
+ | `systemLabel` | `string` | `"System"` | Accessible label for the system option. |
24
+ | `lightLabel` | `string` | `"Light"` | Accessible label for the light option. |
25
+ | `darkLabel` | `string` | `"Dark"` | Accessible label for the dark option. |
26
+ | `systemIcon` | `string` | `"material-symbols:night-sight-auto-sharp"` | Icon name for the system option. |
27
+ | `lightIcon` | `string` | `"radix-icons:sun"` | Icon name for the light option. |
28
+ | `darkIcon` | `string` | `"radix-icons:moon"` | Icon name for the dark option. |
29
+
30
+ ## Basic usage
31
+
32
+ ```vue
33
+ <DisplayThemeSwitch />
34
+
35
+ <!-- Compact, for a header/nav slot -->
36
+ <DisplayThemeSwitch style-class-passthrough="small" />
37
+
38
+ <!-- Translated labels -->
39
+ <DisplayThemeSwitch system-label="Automatique" light-label="Clair" dark-label="Sombre" />
40
+ ```
41
+
42
+ ## Styling
43
+
44
+ No `--display-theme-switch-*` tokens of its own — every visual aspect (colour, marker gradient,
45
+ sizing) is delegated to `TripleToggleSwitchCore`'s public `--triple-toggle-switch-*` tokens. See
46
+ CONSUMER-STYLING.md here and `TripleToggleSwitchCore`'s own CONSUMER-STYLING.md.
47
+
48
+ ## Notes
49
+
50
+ - 2026-09-20 migration: moved from `app/components/display-theme-switch/` (unplaced) into
51
+ `02.molecules/`. Added `systemLabel`/`lightLabel`/`darkLabel`/`systemIcon`/`lightIcon`/`darkIcon`
52
+ props — the three option labels/icons were previously hardcoded English strings with no override
53
+ hook. Removed a large block of dead/duplicate CSS in this component's own `<style>` that either
54
+ no-op-duplicated `TripleToggleSwitchCore`'s own default styling (same selectors, same values) or
55
+ actively bypassed its public `--triple-toggle-switch-*` tokens with hardcoded literals (a
56
+ consumer setting e.g. `--triple-toggle-switch-surface` globally would have been silently
57
+ overridden back). Only the genuine `small`-variant sizing override remains, now driving
58
+ `TripleToggleSwitchCore`'s own public sizing tokens (`--triple-toggle-switch-gap`/`-padding`/
59
+ `-option-padding`/`-icon-size`, added in the same migration) instead of reaching into its
60
+ private `--_form-*`/`--_scheme-icon-font-size` locals directly.
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: GlowingBorder
3
+ description: GlowingBorder decorative wrapper with an animated conic-gradient glow border in 5 colour variants
4
+ type: reference
5
+ ---
6
+
7
+ # GlowingBorder
8
+
9
+ ## Overview
10
+
11
+ `GlowingBorder` wraps its default slot content in a rotating conic-gradient border effect, built
12
+ with `@property`-registered custom properties so the hue rotation animates smoothly. It renders as
13
+ a configurable root element via the `tag` prop.
14
+
15
+ ## Props
16
+
17
+ | Prop | Type | Default | Description |
18
+ |------|------|---------|-------------|
19
+ | `tag` | `"div" \| "p" \| "span" \| "section" \| "article" \| "aside" \| "header" \| "footer" \| "main" \| "nav" \| "ul" \| "ol"` | `"div"` | Root element tag. |
20
+ | `variant` | `"subtle" \| "vivid" \| "silver" \| "steel" \| "green"` | `"subtle"` | Glow colour variant. |
21
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
22
+
23
+ ## Slots
24
+
25
+ | Slot | Description |
26
+ |---|---|
27
+ | `default` | Content rendered inside the glowing border. |
28
+
29
+ ## Basic usage
30
+
31
+ ```vue
32
+ <GlowingBorder variant="vivid">
33
+ <p>Content wrapped in a glowing border</p>
34
+ </GlowingBorder>
35
+ ```
36
+
37
+ ## Styling
38
+
39
+ Border width, radius, surface colour, animation duration, and every variant's 5 gradient colour
40
+ stops are public CSS custom properties — see `CONSUMER-STYLING.md` for the full token table.
41
+
42
+ ```vue
43
+ <GlowingBorder
44
+ variant="steel"
45
+ style="--glowing-border-width: 2px; --glowing-border-animation-duration: 6s;"
46
+ >
47
+ <p>Slower, thinner steel glow</p>
48
+ </GlowingBorder>
49
+ ```
50
+
51
+ ## Motion
52
+
53
+ The glow animation stops automatically under `prefers-reduced-motion: reduce`.
54
+
55
+ ## Notes
56
+
57
+ - 2026-09-20 migration: moved from `app/components/glowing-border/` (unplaced) into
58
+ `01.atoms/animations/glowing-border/`; converted from options-style `defineProps({...})` to
59
+ `interface Props` + `withDefaults`; promoted border width/radius/surface/animation-duration and
60
+ every variant's 5 colour stops (previously hardcoded or unprefixed `--border-width`/`--surface`)
61
+ to public `--glowing-border-*` tokens; renamed internal composition custom properties
62
+ (`--clr-1..5`, `--glow-deg`, `--gradient-glow`) to private `--_clr-1..5`/`--_glow-deg`/
63
+ `--_gradient-glow`; added a `prefers-reduced-motion` guard. No behaviour change beyond the new
64
+ override surface.
65
+ - 2026-09-20: added `green` variant (vibrant, saturated greens: `#00ff87`, `#39ff14`, `#00c853`,
66
+ `#64dd17`, `#00e676`), each stop overridable via `--glowing-border-green-color-{1-5}`.
@@ -0,0 +1,214 @@
1
+ # InputNumberCore Component
2
+
3
+ ## Overview
4
+
5
+ `InputNumberCore` is the low-level native `<input type="number">` primitive for the `05.forms`
6
+ number-input family. It renders a single native number input with left/right button slots (for
7
+ step-down/step-up controls). It has no label, description, or error-message rendering of its
8
+ own — that's composed by the `InputNumberDefault` variant below.
9
+
10
+ Most consumers should reach for **InputNumberDefault** rather than `InputNumberCore` directly.
11
+
12
+ ---
13
+
14
+ ## Props reference
15
+
16
+ > **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`.
17
+
18
+ | Prop (template form) | Type | Default | Notes |
19
+ |------|------|---------|-------|
20
+ | `:id` | `string` | (required) | Applied to the native input. |
21
+ | `:name` | `string` | (required) | Applied to the native input. |
22
+ | `:min` | `number` | (required) | Native `min`. |
23
+ | `:max` | `number` | (required) | Native `max`. |
24
+ | `:step` | `number` | `1` | Native `step`. |
25
+ | `:placeholder` | `string` | `""` | Native `placeholder`. |
26
+ | `:required` | `boolean` | `false` | Native `required`. |
27
+ | `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
28
+ | `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRangeCore`'s `weight` prop. |
29
+ | `: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`. |
30
+ | `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper. |
31
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
32
+ | `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
33
+
34
+ ### v-model
35
+
36
+ `v-model` — `number | readonly number[]`, **required** (`defineModel({ required: true })`). Bound
37
+ directly to the native number input. Required rather than defaulted deliberately — same reasoning
38
+ as `InputRangeCore`'s `v-model` (see that skill doc) — this also satisfies `vue/require-default-prop`.
39
+
40
+ ---
41
+
42
+ ## Slots
43
+
44
+ | Slot | Notes |
45
+ |------|-------|
46
+ | `left` | Rendered before the input (e.g. a step-down button). Adds `has-left-slot` to the wrapper. |
47
+ | `right` | Rendered after the input (e.g. a step-up button). Adds `has-right-slot` to the wrapper. |
48
+
49
+ Any `.input-button-core` inside `.slot` gets its own border/outline stripped and is sized to
50
+ match the input (`aspect-ratio: 1`, `width: var(--input-min-height)`) regardless of which slot
51
+ it's in. The divider border is then added directly to that button element specifically when its
52
+ slot is present — `&.has-left-slot .left-slot .input-button-core { border-right: ... }` and the
53
+ right-hand mirror — so it sits flush against the button's own edge rather than around the
54
+ containing `.slot` div. This makes step buttons read as embedded segments of the same control
55
+ rather than separate floating buttons. The divider reuses the wrapper's own border colour token
56
+ (no separate divider token), same as `InputTextCore`/`InputTextAsNumberWithLabel`.
57
+
58
+ ---
59
+
60
+ ## CSS token API
61
+
62
+ See [CONSUMER-STYLING.md](../../app/components/05.forms/input-number/CONSUMER-STYLING.md) for the
63
+ full table with defaults.
64
+
65
+ **Common tokens:**
66
+ - `--input-number-surface` / `-surface-hover` — wrapper/input/embedded-button background, resting and hover, falls back to `--theme-input-surface`/`-surface-hover`
67
+ - `--input-number-border` / `-border-hover` / `-border-focus` — border colour resting, mouse-hover outline, and `:focus-visible` outline (also the divider colour and the embedded button's own focus outline), falls back to `--theme-border`/`--theme-border-focus`
68
+ - `--input-number-text-color` — input text colour, falls back to `--theme-input-text-color-normal`
69
+ - `--input-number-placeholder-color` — placeholder text colour, falls back to `--theme-input-placeholder`
70
+
71
+ ---
72
+
73
+ ## Fixed 2026-09-22: dead CSS from a missing base class and mismatched selectors
74
+
75
+ `InputNumberCore` migrated to full compliance from a `variants:true`, `eslint_issues:true`,
76
+ score-1/5 state. Several latent bugs were fixed alongside the standard checklist:
77
+
78
+ - The native `<input>` had no static `input-number-core` class — only the dynamic
79
+ `elementClasses` passthrough — so the entire `.input-number-core { ... }` style block (font,
80
+ padding, colour, focus box-shadow) never matched anything. Fixed by adding the class directly.
81
+ - `placeholder` was declared as a prop but never bound on the `<input>` — unlike `InputRangeCore`
82
+ (where a range input genuinely has no placeholder concept and the no-op is intentional and
83
+ documented), a number input *does* support `placeholder` meaningfully, so this was a real gap,
84
+ not a deliberate no-op. Fixed by binding `:placeholder`.
85
+ - The left/right slot divs rendered with classes `slot left`/`slot right`, but the CSS targeted
86
+ `.left-slot`/`.right-slot` and `.has-left-slot`/`.has-right-slot` — neither the slot divs nor
87
+ the wrapper ever carried those exact class names, so the divider-border and flex-alignment
88
+ rules for embedded step buttons never applied. Fixed by renaming the slot divs to
89
+ `slot left-slot`/`slot right-slot` and adding `has-left-slot`/`has-right-slot` to the wrapper
90
+ directly from `slots.left`/`slots.right` (Vue's own `useSlots()`), rather than a fragile,
91
+ cross-component class-toggling relay.
92
+ - Relatedly, `InputNumberDefault` called `updateElementClasses(["has-left-button",
93
+ "has-right-button"])` unconditionally on mount, which permanently added those two classes to
94
+ its own outer `.input-number-with-label` wrapper — a different element than
95
+ `InputNumberCore`'s `.input-number-wrapper`, so the `:has()` selector that was meant to key off
96
+ those classes could never match (the class lived on an ancestor, not a descendant). Replaced
97
+ entirely: `InputNumberCore` now detects an embedded button directly via
98
+ `:has(.left-slot .input-button-core)` / `:has(.right-slot .input-button-core)`, so no
99
+ JS-driven class relay is needed at all.
100
+ - `theme` was typed as an inline `"default" | "success" | "error" | "warning"` union in both
101
+ `InputNumberCore` and `InputNumberDefault` instead of importing the shared `FormUiTheme` type —
102
+ aligned with the rest of `05.forms`.
103
+ - `weight` was passed from `InputNumberDefault` to `InputNumberCore` but `InputNumberCore` had no
104
+ matching prop, so it fell through as a raw, meaningless `weight="normal"` HTML attribute on the
105
+ wrapper `<div>`. Fixed by declaring the prop on `InputNumberCore` too, matching `InputRangeCore`.
106
+ - `defineModel()` had no default, tripping `vue/require-default-prop` — fixed the same way as
107
+ `InputRangeCore`: `defineModel({ required: true })`, not a rule disable or an artificial default.
108
+ - A single-use private token, `--_border`, was inlined to the public
109
+ `var(--input-number-border, var(--theme-border))` directly at its point of use — it had no
110
+ composition or state-swap behind it, just a bare pass-through (see pitfall #20). `--_surface`
111
+ (used twice) and `--_min-width` (composed from a `v-bind()` value) correctly stayed private.
112
+
113
+ ---
114
+
115
+ ## Fixed 2026-09-22 (follow-up): rewritten to visually mirror InputTextCore
116
+
117
+ The first migration pass above only made `InputNumberCore`'s *own* (pre-existing, previously dead)
118
+ CSS active — it didn't reconsider whether that CSS was actually the right design. Once live, it
119
+ was visually inconsistent with the rest of `05.forms`: no border-radius, no hover/focus outline
120
+ state, wrong padding-token family (`--form-textarea-padding-*` instead of the shared input
121
+ padding tokens), and a box-shadow-toggle focus mechanism instead of the `:has()`-based outline
122
+ approach every other input in this library uses. `InputNumberCore` is meant to be a native
123
+ `<input type="number">` sibling to the text-based `InputTextAsNumberWithLabel` variant (see
124
+ `input-text-core.md`'s Variants section) — both should look and behave the same. The CSS was rewritten
125
+ to mirror `InputTextCore` directly:
126
+
127
+ - Added `:input-variant` (`"normal"` / `"outlined"` / `"underlined"`), matching `InputTextCore`'s
128
+ prop and wrapper-class shape (`InputNumberDefault` forwards it the same way
129
+ `InputTextWithLabel` forwards it to `InputTextCore`, including to `InputLabel`).
130
+ - Replaced the flat, unconditional border/radius/box-shadow with `InputTextCore`'s `.normal`
131
+ variant shape: border + `border-radius: var(--form-input-border-radius)` (previously this read
132
+ `var(--form-element-border-width)` — a pre-existing bug that used the *border-width* token as
133
+ the *radius*, coincidentally invisible while the block was dead), plus `:has(input:hover)` /
134
+ `:has(input:focus-visible)` outline states instead of the old `--_focus-box-shadow` custom
135
+ property toggle.
136
+ - `--input-number-border-hover` and `--input-number-border-focus` are new public tokens for
137
+ these two states (both fall back to `--theme-border-focus`, same as `InputTextCore`).
138
+ - Padding moved from `--form-textarea-padding-*` (the wrong token family — textarea, not input)
139
+ to the shared `--input-padding-block`/`--input-padding-inline`/`--input-min-height` tokens every
140
+ other `05.forms` text-like input uses.
141
+ - The embedded step button now gets a hover background (`--input-number-surface-hover`, new
142
+ public token) and a visible `:focus-visible` outline (reusing `--input-number-border-focus`) —
143
+ previously it had neither, an accessibility gap beyond the visual mismatch (checklist item 9).
144
+ - The divider between the input and an embedded button now reuses the border colour
145
+ (`--_border`) directly instead of a separate `--input-number-divider-color` token — removed,
146
+ since `InputTextCore` doesn't have an equivalent separate token either (the border colour
147
+ *is* the divider colour there too).
148
+ - `--_border-hover` and `--_surface-hover` were each used only once with no composition or
149
+ state-swap, so per the public-token rule they were inlined directly
150
+ (`var(--input-number-border-hover, var(--theme-border-focus))` /
151
+ `var(--input-number-surface-hover, var(--theme-input-surface-hover))`) rather than kept as
152
+ private wrappers. `--_border-focus` stayed private since it's now genuinely reused (the
153
+ wrapper's own focus outline *and* the embedded button's focus outline both read it).
154
+
155
+ **Separately found in the same investigation** (not an `InputNumberCore` bug, but what the visual
156
+ comparison was originally checking against): `InputTextAsNumberWithLabel.vue` had its own,
157
+ unrelated pre-existing bug — see `input-text-core.md`'s Variants section for the fix.
158
+
159
+ **Correction, same day**: the first version of this rewrite still didn't visually match — the
160
+ divider border and button sizing (`aspect-ratio: 1`, `width: var(--input-min-height)`) had been
161
+ put on the `.left-slot`/`.right-slot` *wrapper divs*, not on `.input-button-core` itself, unlike
162
+ `InputTextCore`'s equivalent block. That left an unstyled gap between the button and the border
163
+ line instead of a flush edge, and the buttons weren't sized to match the input's height. Fixed by
164
+ moving the divider border and all button sizing/colour rules onto `.input-button-core` directly
165
+ (`&.has-left-slot .left-slot .input-button-core { border-right: ... }`, matching
166
+ `InputTextAsNumberWithLabel`'s `.left-slot .input-button-core { border-right: ... }` exactly)
167
+ rather than the containing `.slot` div. When mirroring another component's CSS structure, check
168
+ *which element* a rule targets, not just that an equivalent rule exists somewhere in the block —
169
+ a border/sizing rule one level off in the DOM tree produces a visually-close-but-not-matching
170
+ result that's easy to miss without a side-by-side screenshot comparison.
171
+
172
+ ---
173
+
174
+ ## Variants
175
+
176
+ ### InputNumberDefault
177
+
178
+ `InputNumberDefault` (`variants/InputNumberDefault.vue`) composes `InputNumberCore` with
179
+ `InputLabel`, `InputDescription`, and `InputError`, plus optional step-down/step-up buttons
180
+ (`InputButtonCore`) wired into the `left`/`right` slots.
181
+
182
+ **Additional props over InputNumberCore:**
183
+
184
+ | Prop (template form) | Type | Default | Notes |
185
+ |------|------|---------|-------|
186
+ | `:label` | `string` | (required) | Rendered via `InputLabel`. |
187
+ | `:error-message` | `object \| string` | (required) | Rendered via `InputError` when `field-has-error` is true. |
188
+ | `:step-down-label` | `string` | `"Step down"` | `button-text` on the step-down `InputButtonCore` — override for localisation. |
189
+ | `:step-up-label` | `string` | `"Step up"` | `button-text` on the step-up `InputButtonCore` — override for localisation. |
190
+
191
+ **Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
192
+ `left`/`right` (forwarded straight through to the underlying `InputNumberCore` — the
193
+ step-down/step-up buttons only render when you use the `left`/`right` slots yourself, since the
194
+ button icon content itself is consumer-supplied).
195
+
196
+ ```vue
197
+ <InputNumberDefault
198
+ name="quantity"
199
+ label="Quantity"
200
+ v-model="quantity"
201
+ :min="1"
202
+ :max="10"
203
+ error-message="Please choose a value between 1 and 10"
204
+ :field-has-error="hasError"
205
+ >
206
+ <template #left><Icon name="mdi:minus" /></template>
207
+ <template #right><Icon name="mdi:plus" /></template>
208
+ </InputNumberDefault>
209
+ ```
210
+
211
+ `v-model` is `number | readonly number[]`, **required** — same as `InputNumberCore` above.
212
+
213
+ Step-down is disabled (`readonly` on its `InputButtonCore`) once the value reaches `min`, and
214
+ step-up once it reaches `max`; clicking either button while at that boundary is a no-op.