srcdev-nuxt-components 9.4.10 → 9.4.12

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 (94) hide show
  1. package/.claude/commands/migrate-component.md +18 -0
  2. package/.claude/component-ledger/audit.json +1 -1
  3. package/.claude/component-ledger/output.html +1 -1
  4. package/.claude/skills/components/input-number-core.md +214 -0
  5. package/.claude/skills/components/input-select-core.md +151 -0
  6. package/.claude/skills/components/input-text-core.md +16 -0
  7. package/.claude/skills/components/input-textarea-core.md +154 -0
  8. package/.claude/skills/components/select-menu.md +195 -0
  9. package/.claude/skills/components/toggle-switch-core.md +150 -0
  10. package/.claude/skills/index.md +5 -0
  11. package/.vscode/srcdev-component-input-number.code-snippets +65 -0
  12. package/.vscode/srcdev-component-input-select.code-snippets +42 -0
  13. package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
  14. package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
  15. package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
  16. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +1 -1
  17. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +1 -1
  18. package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
  19. package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
  20. package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
  21. package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +1 -1
  22. package/app/components/01.atoms/animations/section-parallax/SectionParallax.vue +1 -1
  23. package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +2 -2
  24. package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
  25. package/app/components/01.atoms/card/CardCore.vue +4 -4
  26. package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
  27. package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +2 -2
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +2 -2
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +1 -1
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +1 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +1 -1
  33. package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
  34. package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
  35. package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
  36. package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
  37. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
  38. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
  39. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
  40. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
  41. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +11 -0
  42. package/app/components/02.molecules/pricing-card/PricingCard.vue +3 -1
  43. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +2 -0
  44. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +13 -0
  45. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
  46. package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
  47. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
  48. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
  49. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
  50. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +3 -3
  51. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +3 -3
  52. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +18 -18
  53. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
  54. package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
  55. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
  56. package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
  57. package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
  58. package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
  59. package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
  60. package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
  61. package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
  62. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +2 -2
  63. package/app/components/05.forms/input-range/InputRangeCore.vue +2 -2
  64. package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
  65. package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
  66. package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
  67. package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
  68. package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
  69. package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
  70. package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
  71. package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
  72. package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
  73. package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
  74. package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
  75. package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
  76. package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
  77. package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
  78. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +95 -10
  79. package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
  80. package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
  81. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
  82. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
  83. package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
  84. package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
  85. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
  86. package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
  87. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
  88. package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
  89. package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +4 -4
  90. package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +6 -6
  91. package/app/types/components/index.ts +1 -0
  92. package/app/types/components/select-menu.d.ts +5 -0
  93. package/package.json +1 -1
  94. package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
@@ -259,7 +259,7 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
259
259
  .carousel-flip-demo.carousel-flip .item-container {
260
260
  max-inline-size: 800px;
261
261
  margin-inline: auto;
262
- outline: 1px solid light-dark(var(--slate-08, #666), var(--slate-01, #f5f5f5));
262
+ outline: 1px solid var(--slate-08, #666);
263
263
  padding-block: 6px;
264
264
  padding-inline: 6px;
265
265
  }
@@ -272,12 +272,12 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
272
272
  }
273
273
 
274
274
  .carousel-flip-demo.carousel-flip .item {
275
- background-color: light-dark(var(--slate-05, #e5e5e5), var(--slate-06, #999));
275
+ background-color: var(--slate-05, #e5e5e5);
276
276
  contain: layout style paint;
277
277
  }
278
278
 
279
279
  .carousel-flip-demo.carousel-flip .item:nth-child(odd) {
280
- background-color: light-dark(var(--slate-06, #999), var(--slate-05, #e5e5e5));
280
+ background-color: var(--slate-06, #999);
281
281
  }
282
282
 
283
283
  .custom-carousel-item {
@@ -287,11 +287,11 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
287
287
  justify-content: center;
288
288
  aspect-ratio: 4 / 3;
289
289
  inline-size: 100%;
290
- color: light-dark(#aaa, #333);
290
+ color: #aaa;
291
291
  padding-block: 10px;
292
292
  padding-inline: 10px;
293
293
  border-radius: 4px;
294
- outline: 1px solid light-dark(#00000090, #ffffff90);
294
+ outline: 1px solid #00000090;
295
295
  }
296
296
 
297
297
  .carousel-flip-demo.carousel-flip .controls-container {
@@ -323,7 +323,7 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
323
323
  width: var(--marker-width);
324
324
  height: var(--marker-height);
325
325
  border-radius: var(--marker-border-radius);
326
- background-color: light-dark(var(--slate-05, #e5e5e5), var(--slate-07, #666));
326
+ background-color: var(--slate-05, #e5e5e5);
327
327
  line-height: 3px;
328
328
  transition: all 0.3s linear;
329
329
  border: 1px solid transparent;
@@ -331,13 +331,13 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
331
331
  }
332
332
 
333
333
  .carousel-flip-demo.carousel-flip .btn-marker.active {
334
- background-color: light-dark(var(--slate-012, #000), var(--slate-000, #fff));
334
+ background-color: var(--slate-012, #000);
335
335
  will-change: background-color;
336
336
  }
337
337
 
338
338
  .carousel-flip-demo.carousel-flip .btn-marker:hover,
339
339
  .carousel-flip-demo.carousel-flip .btn-marker:focus-visible {
340
- outline-color: light-dark(#000, #fff);
340
+ outline-color: #000;
341
341
  will-change: background-color, outline-color;
342
342
  }
343
343
  `;
@@ -354,13 +354,13 @@ const Template: StoryFn<CarouselFlipStoryArgs> = (args) => ({
354
354
  Displaying {{ args.carouselDataIds.length }} carousel items with {{ args.transitionSpeed }}ms transition speed
355
355
  </p>
356
356
  <div style="display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap;">
357
- <div style="padding: 4px 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 4px; font-size: 12px; color: light-dark(#333, #ddd);">
357
+ <div style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; color: #333;">
358
358
  Overflow: {{ args.allowCarouselOverflow ? 'Enabled' : 'Disabled' }}
359
359
  </div>
360
- <div style="padding: 4px 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 4px; font-size: 12px; color: light-dark(#333, #ddd);">
360
+ <div style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; color: #333;">
361
361
  Flip Animation: {{ args.useFlipAnimation ? 'Enabled' : 'Disabled' }}
362
362
  </div>
363
- <div style="padding: 4px 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 4px; font-size: 12px; color: light-dark(#333, #ddd);">
363
+ <div style="padding: 4px 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 12px; color: #333;">
364
364
  Spring Effect: {{ args.useSpringEffect ? 'Enabled' : 'Disabled' }}
365
365
  </div>
366
366
  </div>
@@ -513,7 +513,7 @@ const RichContentTemplate: StoryFn<CarouselFlipStoryArgs> = (args) => ({
513
513
  .carousel-flip-demo.carousel-flip .item-container {
514
514
  max-inline-size: 800px;
515
515
  margin-inline: auto;
516
- outline: 1px solid light-dark(var(--slate-08, #666), var(--slate-01, #f5f5f5));
516
+ outline: 1px solid var(--slate-08, #666);
517
517
  padding-block: 6px;
518
518
  padding-inline: 6px;
519
519
  }
@@ -526,12 +526,12 @@ const RichContentTemplate: StoryFn<CarouselFlipStoryArgs> = (args) => ({
526
526
  }
527
527
 
528
528
  .carousel-flip-demo.carousel-flip .item {
529
- background-color: light-dark(var(--slate-05, #e5e5e5), var(--slate-06, #999));
529
+ background-color: var(--slate-05, #e5e5e5);
530
530
  contain: layout style paint;
531
531
  }
532
532
 
533
533
  .carousel-flip-demo.carousel-flip .item:nth-child(odd) {
534
- background-color: light-dark(var(--slate-06, #999), var(--slate-05, #e5e5e5));
534
+ background-color: var(--slate-06, #999);
535
535
  }
536
536
  `;
537
537
  document.head.appendChild(style);
@@ -682,16 +682,16 @@ const AnimationShowcaseTemplate: StoryFn<CarouselFlipStoryArgs> = (args) => ({
682
682
  Testing different animation combinations and timing
683
683
  </p>
684
684
  <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(120px, 1fr)); gap: 8px; margin-bottom: 20px;">
685
- <div style="padding: 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 6px; text-align: center; font-size: 12px; color: light-dark(#333, #ddd); background: args.useFlipAnimation ? 'light-dark(#e7f5e7, #2d4a2d)' : 'transparent';">
685
+ <div style="padding: 8px; border: 1px solid #ccc; border-radius: 6px; text-align: center; font-size: 12px; color: #333; background: args.useFlipAnimation ? '#e7f5e7' : 'transparent';">
686
686
  Flip: {{ args.useFlipAnimation ? '✓' : '✗' }}
687
687
  </div>
688
- <div style="padding: 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 6px; text-align: center; font-size: 12px; color: light-dark(#333, #ddd); background: args.useSpringEffect ? 'light-dark(#e7f5e7, #2d4a2d)' : 'transparent';">
688
+ <div style="padding: 8px; border: 1px solid #ccc; border-radius: 6px; text-align: center; font-size: 12px; color: #333; background: args.useSpringEffect ? '#e7f5e7' : 'transparent';">
689
689
  Spring: {{ args.useSpringEffect ? '✓' : '✗' }}
690
690
  </div>
691
- <div style="padding: 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 6px; text-align: center; font-size: 12px; color: light-dark(#333, #ddd); background: args.allowCarouselOverflow ? 'light-dark(#e7f5e7, #2d4a2d)' : 'transparent';">
691
+ <div style="padding: 8px; border: 1px solid #ccc; border-radius: 6px; text-align: center; font-size: 12px; color: #333; background: args.allowCarouselOverflow ? '#e7f5e7' : 'transparent';">
692
692
  Overflow: {{ args.allowCarouselOverflow ? '✓' : '✗' }}
693
693
  </div>
694
- <div style="padding: 8px; border: 1px solid light-dark(#ccc, #666); border-radius: 6px; text-align: center; font-size: 12px; color: light-dark(#333, #ddd);">
694
+ <div style="padding: 8px; border: 1px solid #ccc; border-radius: 6px; text-align: center; font-size: 12px; color: #333;">
695
695
  Speed: {{ args.transitionSpeed }}ms
696
696
  </div>
697
697
  </div>
@@ -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" };