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
@@ -53,11 +53,11 @@ Same shape, falls back to the inverted-surface slots instead:
53
53
 
54
54
  ### Tertiary
55
55
 
56
- Same shape again; resting surface is transparent by default in dark mode (see note below):
56
+ Same shape again:
57
57
 
58
58
  | Token | Falls back to |
59
59
  |---|---|
60
- | `--input-button-tertiary-surface` | `light-dark(var(--slate-01), transparent)` |
60
+ | `--input-button-tertiary-surface` | `var(--slate-01)` |
61
61
  | `--input-button-tertiary-text` | `var(--theme-text)` |
62
62
  | `--input-button-tertiary-border` | the resolved surface above (flat) |
63
63
  | `--input-button-tertiary-ring` / `-ring-hover` | `transparent` |
@@ -66,11 +66,6 @@ Same shape again; resting surface is transparent by default in dark mode (see no
66
66
  | `--input-button-tertiary-border-focus` | `var(--theme-border-focus)` |
67
67
  | `--input-button-tertiary-ring-focus` | `var(--theme-ring)` |
68
68
 
69
- > `--input-button-tertiary-surface`'s dark-mode default is literal `transparent`. `color-mix()`
70
- > still produces a correct darker hover from that — mixing `transparent` with `black` lowers the
71
- > alpha channel rather than the lightness, so hover reads as a faint dark tint over whatever's
72
- > behind the button, not a flat fill swap.
73
-
74
69
  `.is-pending` always dims the *primary* surface regardless of variant — a pre-existing quirk, not
75
70
  something this token set changes.
76
71
 
@@ -196,11 +196,7 @@ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough)
196
196
  }
197
197
 
198
198
  &.tertiary {
199
- /* --_surface can resolve to literal transparent (dark mode default) — color-mix() still
200
- darkens correctly here: mixing transparent with black lowers the alpha channel rather
201
- than the lightness, so it reads as a faint dark tint over whatever's behind the button
202
- instead of a flat fill. Same formula, no special-casing needed. */
203
- --_surface: var(--input-button-tertiary-surface, light-dark(var(--slate-01), transparent));
199
+ --_surface: var(--input-button-tertiary-surface, var(--slate-01));
204
200
  --_surface-hover: var(
205
201
  --input-button-tertiary-surface-hover,
206
202
  color-mix(in oklab, var(--_surface) 85%, black)
@@ -7,16 +7,36 @@ to override just this component; leave it unset and it inherits the shared `--th
7
7
  `--theme-*` token every other themed component also falls back to. See
8
8
  `theming-component-token-pattern.md` for why this two-tier shape exists.
9
9
 
10
+ This shape mirrors `InputTextCore`'s token set (see its own CONSUMER-STYLING.md) — `InputNumberCore`
11
+ is a native `<input type="number">` sibling to the text-based `InputTextAsNumberWithLabel` variant,
12
+ and both are meant to look and behave the same way. Border colour also doubles as the divider
13
+ colour between the input and an embedded left/right `InputButtonCore` — there's no separate divider
14
+ token, same as `InputTextCore`.
15
+
10
16
  | Token | Falls back to | Controls |
11
17
  |---|---|---|
12
- | `--input-number-surface` | `var(--theme-input-surface)` | Wrapper and input background |
13
- | `--input-number-border` | `var(--theme-border)` | Border colour |
18
+ | `--input-number-surface` | `var(--theme-input-surface)` | Wrapper, input, and embedded-button background |
19
+ | `--input-number-surface-hover` | `var(--theme-input-surface-hover)` | Embedded left/right button background on hover |
20
+ | `--input-number-border` | `var(--theme-border)` | Border/divider colour (`normal`/`underlined` variant border, and the divider between the input and an embedded button) |
21
+ | `--input-number-border-hover` | `var(--theme-border-focus)` | Wrapper outline colour on mouse hover (`normal` variant only) |
22
+ | `--input-number-border-focus` | `var(--theme-border-focus)` | Wrapper outline colour on `:focus-visible` (`normal` variant), and the embedded button's own `:focus-visible` outline |
14
23
  | `--input-number-text-color` | `var(--theme-input-text-color-normal)` | Input text colour |
15
- | `--input-number-divider-color` | `var(--theme-input-surface-hover)` | Divider between the input and an embedded left/right `InputButtonCore` |
24
+ | `--input-number-placeholder-color` | `var(--theme-input-placeholder)` | Placeholder text colour |
25
+
26
+ Geometry (`--input-padding-block`, `--input-padding-inline`, `--input-min-height`,
27
+ `--form-input-border-radius`, `--input-font-size`, etc.) is shared across every `05.forms`
28
+ component and untouched by this — see `theming-form-geometry-tokens.md`.
29
+
30
+ ---
31
+
32
+ ## Input variant
16
33
 
17
- Geometry (`--form-textarea-padding-*`, `--input-font-size`, `--input-element-line-height`, etc.)
18
- is shared across every `05.forms` component and untouched by this — see
19
- `theming-form-geometry-tokens.md`.
34
+ `:input-variant="'normal' | 'outlined' | 'underlined'"` (default `"normal"`) — same prop as
35
+ `InputTextCore`. Only `normal` (bordered box, with hover/focus outline) and `underlined`
36
+ (bottom-border only) have CSS today; `outlined` has no styling in `InputTextCore` either (no
37
+ `&.outlined` rule exists there), so passing it renders with no border/outline at all — a
38
+ pre-existing gap in the type, not something specific to `InputNumberCore`. Declared for prop-shape
39
+ consistency with the rest of `05.forms`.
20
40
 
21
41
  ---
22
42
 
@@ -1,6 +1,11 @@
1
1
  <template>
2
- <div class="input-number-wrapper" :data-theme="theme" :data-invalid="fieldHasError ? '' : null">
3
- <div v-if="slots.left" class="slot left">
2
+ <div
3
+ class="input-number-wrapper"
4
+ :data-theme="theme"
5
+ :data-invalid="fieldHasError ? '' : null"
6
+ :class="[inputVariant, { 'has-left-slot': slots.left, 'has-right-slot': slots.right }]"
7
+ >
8
+ <div v-if="slots.left" class="slot left-slot">
4
9
  <slot name="left"></slot>
5
10
  </div>
6
11
 
@@ -15,19 +20,22 @@
15
20
  :min
16
21
  :max
17
22
  :step
18
- :class="[elementClasses]"
23
+ :placeholder
24
+ :class="['input-number-core', `input-number--${weight}`, elementClasses]"
19
25
  inputmode="numeric"
20
26
  pattern="[0-9]+"
21
27
  :aria-describedby
22
28
  />
23
29
  </div>
24
- <div v-if="slots.right" class="slot right">
30
+ <div v-if="slots.right" class="slot right-slot">
25
31
  <slot name="right"></slot>
26
32
  </div>
27
33
  </div>
28
34
  </template>
29
35
 
30
36
  <script setup lang="ts">
37
+ import type { FormUiTheme, FormWeight, InputUiVariant } from "~/types/forms/types.forms";
38
+
31
39
  interface Props {
32
40
  id: string;
33
41
  name: string;
@@ -36,7 +44,9 @@ interface Props {
36
44
  step?: number;
37
45
  placeholder?: string;
38
46
  required?: boolean;
39
- theme?: "default" | "success" | "error" | "warning";
47
+ theme?: FormUiTheme;
48
+ weight?: FormWeight;
49
+ inputVariant?: InputUiVariant;
40
50
  fieldHasError?: boolean;
41
51
  styleClassPassthrough?: string | string[];
42
52
  ariaDescribedby?: string;
@@ -47,6 +57,8 @@ const props = withDefaults(defineProps<Props>(), {
47
57
  placeholder: "",
48
58
  required: false,
49
59
  theme: "default",
60
+ weight: "normal",
61
+ inputVariant: "normal",
50
62
  fieldHasError: false,
51
63
  styleClassPassthrough: () => [],
52
64
  ariaDescribedby: "",
@@ -54,7 +66,9 @@ const props = withDefaults(defineProps<Props>(), {
54
66
 
55
67
  const slots = useSlots();
56
68
 
57
- const modelValue = defineModel<number | readonly number[]>();
69
+ const modelValue = defineModel<number | readonly number[]>({
70
+ required: true,
71
+ });
58
72
 
59
73
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
60
74
  const minLength = computed(() => `${props.max.toString().length + 1}em`);
@@ -62,108 +76,134 @@ const minLength = computed(() => `${props.max.toString().length + 1}em`);
62
76
 
63
77
  <style lang="css">
64
78
  @layer components {
65
- .input-number-wrapper {
66
- --_focus-box-shadow: var(--box-shadow-off);
67
- --_min-width: v-bind(minLength);
79
+ .input-number-wrapper {
80
+ --_min-width: v-bind(minLength);
68
81
 
69
- /* Public --input-number-* tokens, inline-fallback to the shared --theme-* tokens (see
82
+ /* Public --input-number-* tokens, inline-fallback to the shared --theme-* tokens (see
70
83
  theming-component-token-pattern.md) — overriding one here doesn't touch every other themed
71
- input/control that also reads --theme-input-surface/--theme-border. */
72
- --_surface: var(--input-number-surface, var(--theme-input-surface));
73
- --_border: var(--input-number-border, var(--theme-border));
84
+ input/control that also reads --theme-input-surface/--theme-border. Mirrors InputTextCore's
85
+ token shape so a native number input and the text-based InputTextAsNumberWithLabel variant
86
+ look and behave the same way (see input-number-core.md's "Fixed 2026-09-22" note). */
87
+ --_surface: var(--input-number-surface, var(--theme-input-surface));
88
+ --_border: var(--input-number-border, var(--theme-border));
89
+ --_border-focus: var(--input-number-border-focus, var(--theme-border-focus));
74
90
 
75
- display: flex;
76
- align-items: center;
91
+ display: flex;
92
+ align-items: center;
77
93
 
78
- width: fit-content;
94
+ width: fit-content;
79
95
 
80
- background-color: var(--_surface);
81
- border-radius: var(--form-element-border-width);
82
- border: var(--form-element-border-width) solid var(--_border);
83
- outline: var(--form-element-outline-width) solid transparent;
84
- box-shadow: var(--_focus-box-shadow);
85
-
86
- .slot {
87
- display: inline-block;
96
+ background-color: var(--_surface);
97
+ overflow: hidden;
98
+ transition: all var(--theme-form-transition-duration) ease-in-out;
99
+ position: relative;
100
+ z-index: 2;
101
+
102
+ &.normal {
103
+ border: var(--form-element-border-width) solid var(--_border);
104
+ border-radius: var(--form-input-border-radius);
105
+ outline: var(--form-element-outline-width) solid transparent;
106
+
107
+ &:has(input:hover) {
108
+ outline: var(--form-element-outline-width-focus) solid
109
+ var(--input-number-border-hover, var(--theme-border-focus));
110
+ outline-offset: var(--form-element-outline-offset-focus);
111
+ }
88
112
 
89
- .icon {
90
- font-weight: 900;
113
+ &:has(input:focus-visible) {
114
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
115
+ outline-offset: var(--form-element-outline-offset-focus);
116
+ }
91
117
  }
92
- }
93
118
 
94
- &.has-left-slot {
95
- .left-slot {
96
- display: flex;
97
- align-items: center;
119
+ &.underlined {
120
+ border-bottom: var(--form-element-border-bottom-width-underlined) solid var(--_border);
98
121
  }
99
- }
100
122
 
101
- &.has-right-slot {
102
- .right-slot {
103
- display: flex;
104
- align-items: center;
105
- }
106
- }
123
+ .slot {
124
+ display: inline-block;
107
125
 
108
- .input-number-core {
109
- touch-action: manipulation;
110
- background-color: transparent;
111
- border: none;
112
- outline: none;
113
- box-shadow: none;
126
+ .icon {
127
+ font-weight: 900;
128
+ }
114
129
 
115
- background-color: var(--_surface);
116
- color: var(--input-number-text-color, var(--theme-input-text-color-normal));
117
- font-family: var(--font-family);
118
- font-size: var(--input-font-size);
119
- line-height: var(--input-element-line-height);
120
-
121
- padding-inline: var(--form-textarea-padding-inline);
122
- padding-block-start: var(--form-textarea-padding-block-start);
123
- padding-block-end: var(--form-textarea-padding-block-end);
124
- text-align: center;
125
- min-width: var(--_min-width);
126
-
127
- &:focus-visible {
128
- --_focus-box-shadow: var(--box-shadow-on);
130
+ .input-button-core {
131
+ background-color: var(--_surface);
132
+ color: var(--input-number-text-color, var(--theme-input-text-color-normal));
133
+ border: none;
134
+ outline: none;
135
+ aspect-ratio: 1;
136
+ border-radius: 0;
137
+ width: var(--input-min-height);
138
+
139
+ &:hover {
140
+ background-color: var(--input-number-surface-hover, var(--theme-input-surface-hover));
141
+ }
142
+
143
+ &:is(:focus-visible) {
144
+ outline: var(--form-element-outline-width-focus) solid var(--_border-focus);
145
+ outline-offset: -4px;
146
+ }
147
+
148
+ &.icon-only {
149
+ .btn-icon {
150
+ margin: 0;
151
+ }
152
+ }
153
+ }
129
154
  }
130
155
 
131
- &::placeholder,
132
- &::-webkit-input-placeholder {
133
- font-family: var(--font-family);
134
- font-size: var(--font-size);
135
- font-style: italic;
136
- font-weight: 400;
156
+ &.has-left-slot {
157
+ .left-slot {
158
+ display: flex;
159
+ align-items: center;
160
+ }
137
161
  }
138
- }
139
162
 
140
- &:has(.has-left-button),
141
- &:has(.has-right-button) {
142
- .slot {
143
- .input-button-core {
144
- border: initial;
145
- border-radius: 0;
146
- outline: initial;
147
- box-shadow: unset;
163
+ &.has-right-slot {
164
+ .right-slot {
165
+ display: flex;
166
+ align-items: center;
148
167
  }
149
168
  }
150
169
 
151
- .left-slot {
152
- margin-inline-end: 0;
153
- border-right: 2px solid var(--input-number-divider-color, var(--theme-input-surface-hover));
170
+ &.has-left-slot .left-slot .input-button-core {
171
+ border-right: var(--form-element-border-width) solid var(--_border);
154
172
  }
155
173
 
156
- .right-slot {
157
- margin-inline-end: 0;
158
- border-left: 2px solid var(--input-number-divider-color, var(--theme-input-surface-hover));
174
+ &.has-right-slot .right-slot .input-button-core {
175
+ border-left: var(--form-element-border-width) solid var(--_border);
176
+ }
177
+
178
+ .input-number-core {
179
+ all: unset;
180
+ touch-action: manipulation;
181
+
182
+ color: var(--input-number-text-color, var(--theme-input-text-color-normal));
183
+ font-family: var(--font-family);
184
+ font-size: var(--input-font-size);
185
+
186
+ padding-block: var(--input-padding-block);
187
+ padding-inline: var(--input-padding-inline);
188
+ min-height: var(--input-min-height);
189
+ min-width: var(--_min-width);
190
+ text-align: center;
191
+
192
+ &::placeholder,
193
+ &::-webkit-input-placeholder {
194
+ color: var(--input-number-placeholder-color, var(--theme-input-placeholder));
195
+ font-size: var(--theme-input-placeholder-font-size);
196
+ font-style: italic;
197
+ line-height: 1;
198
+ font-weight: normal;
199
+ }
159
200
  }
160
201
  }
161
- }
162
202
 
163
- input[type="number"]::-webkit-inner-spin-button,
164
- input[type="number"]::-webkit-outer-spin-button {
165
- -webkit-appearance: none;
166
- margin: 0;
167
- }
203
+ input[type="number"]::-webkit-inner-spin-button,
204
+ input[type="number"]::-webkit-outer-spin-button {
205
+ -webkit-appearance: none;
206
+ margin: 0;
207
+ }
168
208
  }
169
209
  </style>
@@ -0,0 +1,92 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import { ref } from "vue";
3
+ import StorybookComponent from "../InputNumberCore.vue";
4
+
5
+ interface InputNumberCoreStoryArgs {
6
+ id: string;
7
+ name: string;
8
+ min: number;
9
+ max: number;
10
+ step: number;
11
+ placeholder: string;
12
+ required: boolean;
13
+ fieldHasError: boolean;
14
+ weight: string;
15
+ inputVariant: string;
16
+ }
17
+
18
+ export default {
19
+ title: "Components/Forms/Input Number/InputNumberCore",
20
+ component: StorybookComponent,
21
+ argTypes: {
22
+ id: { control: "text", table: { category: "Basic" } },
23
+ name: { control: "text", table: { category: "Basic" } },
24
+ min: { control: "number", table: { category: "Basic" } },
25
+ max: { control: "number", table: { category: "Basic" } },
26
+ step: { control: "number", table: { category: "Basic" } },
27
+ placeholder: { control: "text", table: { category: "Basic" } },
28
+ required: { control: "boolean", table: { category: "Basic" } },
29
+ fieldHasError: { control: "boolean", table: { category: "States" } },
30
+ weight: { control: "text", table: { category: "Styling" } },
31
+ inputVariant: {
32
+ control: { type: "select" },
33
+ options: ["normal", "outlined", "underlined"],
34
+ table: { category: "Styling" },
35
+ },
36
+ },
37
+ args: {
38
+ id: "number-default",
39
+ name: "numberDefault",
40
+ min: 0,
41
+ max: 100,
42
+ step: 1,
43
+ placeholder: "",
44
+ required: false,
45
+ fieldHasError: false,
46
+ weight: "normal",
47
+ inputVariant: "normal",
48
+ },
49
+ } as Meta<InputNumberCoreStoryArgs>;
50
+
51
+ const Template: StoryFn<InputNumberCoreStoryArgs> = (args) => ({
52
+ components: { StorybookComponent },
53
+ setup() {
54
+ const modelValue = ref(50);
55
+ return { args, modelValue };
56
+ },
57
+ template: `
58
+ <div style="margin: 36px; max-width: 320px;">
59
+ <StorybookComponent v-bind="args" v-model="modelValue" />
60
+ </div>
61
+ `,
62
+ });
63
+
64
+ export const Default = Template.bind({});
65
+
66
+ export const ErrorState = Template.bind({});
67
+ ErrorState.args = { fieldHasError: true };
68
+
69
+ const WithStepButtonsTemplate: StoryFn<InputNumberCoreStoryArgs> = (args) => ({
70
+ components: { StorybookComponent },
71
+ setup() {
72
+ const modelValue = ref(50);
73
+ return { args, modelValue };
74
+ },
75
+ template: `
76
+ <div style="margin: 36px; max-width: 320px;">
77
+ <StorybookComponent v-bind="args" v-model="modelValue">
78
+ <template #left>
79
+ <button type="button" @click="modelValue = Math.max(args.min, modelValue - args.step)">−</button>
80
+ </template>
81
+ <template #right>
82
+ <button type="button" @click="modelValue = Math.min(args.max, modelValue + args.step)">+</button>
83
+ </template>
84
+ </StorybookComponent>
85
+ </div>
86
+ `,
87
+ });
88
+
89
+ export const WithStepButtons = WithStepButtonsTemplate.bind({});
90
+
91
+ export const Underlined = WithStepButtonsTemplate.bind({});
92
+ Underlined.args = { inputVariant: "underlined" };
@@ -0,0 +1,119 @@
1
+ // https://nuxt.com/docs/getting-started/testing#unit-testing
2
+ import type { VueWrapper } from "@vue/test-utils";
3
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
+ import ComponentUnderTest from "../InputNumberCore.vue";
5
+
6
+ const initialPropsData = {
7
+ id: "test-number",
8
+ name: "testName",
9
+ min: 0,
10
+ max: 100,
11
+ modelValue: 50,
12
+ };
13
+
14
+ let wrapper: VueWrapper<InstanceType<typeof ComponentUnderTest>>;
15
+ const wrapperFactory = (propsData = {}, slots = {}) => {
16
+ const mockPropsData = { ...initialPropsData, ...propsData };
17
+
18
+ return mountSuspended(ComponentUnderTest, {
19
+ attachTo: document.body,
20
+ props: mockPropsData,
21
+ slots,
22
+ });
23
+ };
24
+
25
+ describe("InputNumberCore", () => {
26
+ it("mounts without error", async () => {
27
+ wrapper = await wrapperFactory();
28
+ expect(wrapper.vm).toBeTruthy();
29
+ });
30
+
31
+ it("renders a native number input with the given min/max/step", async () => {
32
+ wrapper = await wrapperFactory({ min: 10, max: 90, step: 5 });
33
+ const input = wrapper.find("input");
34
+
35
+ expect(input.attributes("type")).toBe("number");
36
+ expect(input.attributes("min")).toBe("10");
37
+ expect(input.attributes("max")).toBe("90");
38
+ expect(input.attributes("step")).toBe("5");
39
+ });
40
+
41
+ it("defaults step to 1", async () => {
42
+ wrapper = await wrapperFactory();
43
+ expect(wrapper.find("input").attributes("step")).toBe("1");
44
+ });
45
+
46
+ it("binds v-model to the input value", async () => {
47
+ wrapper = await wrapperFactory({ modelValue: 42 });
48
+ expect(wrapper.find("input").element.value).toBe("42");
49
+ });
50
+
51
+ it("forwards placeholder", async () => {
52
+ wrapper = await wrapperFactory({ placeholder: "Enter a quantity" });
53
+ expect(wrapper.find("input").attributes("placeholder")).toBe("Enter a quantity");
54
+ });
55
+
56
+ it("forwards aria-describedby", async () => {
57
+ wrapper = await wrapperFactory({ ariaDescribedby: "some-description-id" });
58
+ expect(wrapper.find("input").attributes("aria-describedby")).toBe("some-description-id");
59
+ });
60
+
61
+ it("sets data-theme from the theme prop", async () => {
62
+ wrapper = await wrapperFactory({ theme: "error" });
63
+ expect(wrapper.find(".input-number-wrapper").attributes("data-theme")).toBe("error");
64
+ });
65
+
66
+ it("sets data-invalid when fieldHasError is true", async () => {
67
+ wrapper = await wrapperFactory({ fieldHasError: true });
68
+ expect(wrapper.find(".input-number-wrapper").attributes("data-invalid")).toBe("");
69
+ });
70
+
71
+ it("does not render the left/right slot wrappers when the slots are not used", async () => {
72
+ wrapper = await wrapperFactory();
73
+ expect(wrapper.find(".slot.left-slot").exists()).toBe(false);
74
+ expect(wrapper.find(".slot.right-slot").exists()).toBe(false);
75
+ expect(wrapper.find(".input-number-wrapper").classes()).not.toContain("has-left-slot");
76
+ expect(wrapper.find(".input-number-wrapper").classes()).not.toContain("has-right-slot");
77
+ });
78
+
79
+ it("renders the left/right slots when provided", async () => {
80
+ wrapper = await wrapperFactory(
81
+ {},
82
+ {
83
+ left: '<button data-testid="left-slot">-</button>',
84
+ right: '<button data-testid="right-slot">+</button>',
85
+ }
86
+ );
87
+
88
+ expect(wrapper.find(".slot.left-slot [data-testid='left-slot']").exists()).toBe(true);
89
+ expect(wrapper.find(".slot.right-slot [data-testid='right-slot']").exists()).toBe(true);
90
+ expect(wrapper.find(".input-number-wrapper").classes()).toContain("has-left-slot");
91
+ expect(wrapper.find(".input-number-wrapper").classes()).toContain("has-right-slot");
92
+ });
93
+
94
+ it("applies the weight class from the weight prop", async () => {
95
+ wrapper = await wrapperFactory({ weight: "wght-700" });
96
+ expect(wrapper.find("input").classes()).toContain("input-number--wght-700");
97
+ });
98
+
99
+ it("applies styleClassPassthrough to the input", async () => {
100
+ wrapper = await wrapperFactory({ styleClassPassthrough: ["custom-number-class"] });
101
+ expect(wrapper.find("input").classes()).toContain("custom-number-class");
102
+ });
103
+
104
+ it("sets required attribute when required is true", async () => {
105
+ wrapper = await wrapperFactory({ required: true });
106
+ expect(wrapper.find("input").attributes("required")).toBeDefined();
107
+ });
108
+
109
+ it("defaults inputVariant to normal", async () => {
110
+ wrapper = await wrapperFactory();
111
+ expect(wrapper.find(".input-number-wrapper").classes()).toContain("normal");
112
+ });
113
+
114
+ it("applies inputVariant as a class on the wrapper", async () => {
115
+ wrapper = await wrapperFactory({ inputVariant: "underlined" });
116
+ expect(wrapper.find(".input-number-wrapper").classes()).toContain("underlined");
117
+ expect(wrapper.find(".input-number-wrapper").classes()).not.toContain("normal");
118
+ });
119
+ });