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.
- package/.claude/commands/migrate-component.md +38 -6
- package/.claude/component-ledger/audit.json +1 -1
- package/.claude/component-ledger/build.mjs +26 -0
- package/.claude/component-ledger/output.html +8 -3
- package/.claude/component-ledger/template.html +7 -2
- package/.claude/skills/component-dynamic-slots.md +1 -1
- package/.claude/skills/components/alert-mask-core.md +84 -0
- package/.claude/skills/components/canvas-switcher.md +1 -1
- package/.claude/skills/components/carousel-flip.md +112 -168
- package/.claude/skills/components/container-glow.md +123 -0
- package/.claude/skills/components/dashboard-quad-grid.md +98 -0
- package/.claude/skills/components/dashboard-stats-grid.md +103 -0
- package/.claude/skills/components/display-theme-switch.md +60 -0
- package/.claude/skills/components/glowing-border.md +66 -0
- package/.claude/skills/components/input-number-core.md +214 -0
- package/.claude/skills/components/input-range-core.md +142 -0
- package/.claude/skills/components/input-select-core.md +151 -0
- package/.claude/skills/components/input-text-core.md +16 -0
- package/.claude/skills/components/input-textarea-core.md +154 -0
- package/.claude/skills/components/section-parallax.md +32 -10
- package/.claude/skills/components/select-menu.md +195 -0
- package/.claude/skills/components/toggle-switch-core.md +150 -0
- package/.claude/skills/components/ui-block-decorated.md +56 -0
- package/.claude/skills/components/wipe-away-vertical.md +98 -0
- package/.claude/skills/icon-sets.md +1 -1
- package/.claude/skills/index.md +15 -1
- package/.vscode/css-custom-data.json +38 -0
- package/.vscode/settings.json +3 -0
- package/.vscode/srcdev-component-alert-mask-core.code-snippets +31 -0
- package/.vscode/srcdev-component-carousel-flip.code-snippets +41 -0
- package/.vscode/srcdev-component-container-glow.code-snippets +33 -0
- package/.vscode/srcdev-component-dashboard-quad-grid.code-snippets +36 -0
- package/.vscode/srcdev-component-dashboard-stats-grid.code-snippets +35 -0
- package/.vscode/srcdev-component-display-theme-switch.code-snippets +27 -0
- package/.vscode/srcdev-component-glowing-border.code-snippets +23 -0
- package/.vscode/srcdev-component-input-number.code-snippets +65 -0
- package/.vscode/srcdev-component-input-range.code-snippets +85 -0
- package/.vscode/srcdev-component-input-select.code-snippets +42 -0
- package/.vscode/srcdev-component-input-textarea.code-snippets +40 -0
- package/.vscode/srcdev-component-section-parallax.code-snippets +27 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +65 -0
- package/.vscode/srcdev-component-toggle-switch.code-snippets +49 -0
- package/.vscode/srcdev-component-ui-block-decorated.code-snippets +24 -0
- package/.vscode/srcdev-component-wipe-away-vertical.code-snippets +25 -0
- package/README.md +18 -37
- package/app/components/{alert-mask → 01.atoms/alert-mask}/AlertMaskCore.vue +16 -17
- package/app/components/01.atoms/alert-mask/CONSUMER-STYLING.md +46 -0
- package/app/components/01.atoms/alert-mask/stories/AlertMaskCore.stories.ts +79 -0
- package/app/components/{alert-mask → 01.atoms/alert-mask}/tests/AlertMaskCore.spec.ts +10 -10
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +86 -0
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +299 -0
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +175 -0
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +131 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +14 -0
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +61 -0
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +132 -0
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +165 -0
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +94 -0
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +3 -0
- package/app/components/01.atoms/animations/marquee-scroller/CONSUMER-STYLING.md +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/MarqueeScroller.vue +1 -1
- package/app/components/01.atoms/animations/marquee-scroller/stories/MarqueeScroller.stories.ts +1 -1
- package/app/components/01.atoms/animations/section-parallax/CONSUMER-STYLING.md +44 -0
- package/app/components/{parallax → 01.atoms/animations/section-parallax}/SectionParallax.vue +11 -10
- package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +98 -0
- package/app/components/01.atoms/animations/section-parallax/tests/SectionParallax.spec.ts +102 -0
- package/app/components/01.atoms/animations/section-parallax/tests/__snapshots__/SectionParallax.spec.ts.snap +9 -0
- package/app/components/01.atoms/animations/view-timeline/CONSUMER-STYLING.md +70 -0
- package/app/components/01.atoms/animations/view-timeline/WipeAwayVertical.vue +181 -0
- package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +269 -0
- package/app/components/01.atoms/animations/view-timeline/tests/WipeAwayVertical.spec.ts +127 -0
- package/app/components/01.atoms/canvas-switcher/CONSUMER-STYLING.md +3 -3
- package/app/components/01.atoms/canvas-switcher/CanvasSwitcher.vue +2 -2
- package/app/components/01.atoms/canvas-switcher/stories/CanvasSwitcher.stories.ts +1 -1
- package/app/components/01.atoms/card/CardCore.vue +4 -4
- package/app/components/01.atoms/clipped-panel/CONSUMER-STYLING.md +2 -2
- package/app/components/01.atoms/clipped-panel/ClippedPanel.vue +2 -2
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +42 -0
- package/app/components/{layout-grids/LayoutGridA.vue → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue} +23 -13
- package/app/components/{layout-grids/stories/LayoutGridA.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts} +151 -23
- package/app/components/{layout-grids/tests/LayoutGridA.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts} +25 -25
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +41 -0
- package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +133 -0
- package/app/components/{layout-grids/stories/LayoutGridB.stories.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts} +24 -24
- package/app/components/{layout-grids/tests/LayoutGridB.spec.ts → 01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts} +23 -23
- package/app/components/01.atoms/display-tooltip/CONSUMER-STYLING.md +8 -8
- package/app/components/01.atoms/display-tooltip/DisplayTooltip.vue +8 -8
- package/app/components/01.atoms/pop-over/CONSUMER-STYLING.md +6 -6
- package/app/components/01.atoms/pop-over/PopOver.vue +6 -6
- package/app/components/01.atoms/ui-block-decorated/CONSUMER-STYLING.md +39 -0
- package/app/components/01.atoms/ui-block-decorated/UiBlockDecorated.vue +141 -0
- package/app/components/01.atoms/ui-block-decorated/stories/UiBlockDecorated.stories.ts +148 -0
- package/app/components/01.atoms/ui-block-decorated/tests/UiBlockDecorated.spec.ts +80 -0
- package/app/components/01.atoms/ui-block-decorated/tests/__snapshots__/UiBlockDecorated.spec.ts.snap +3 -0
- package/app/components/02.molecules/alert-masked-content/tests/AlertMaskedContent.spec.ts +6 -6
- package/app/components/02.molecules/display-theme-switch/CONSUMER-STYLING.md +34 -0
- package/app/components/02.molecules/display-theme-switch/DisplayThemeSwitch.vue +102 -0
- package/app/components/02.molecules/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +115 -0
- package/app/components/02.molecules/display-theme-switch/tests/DisplayThemeSwitch.spec.ts +86 -0
- package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +1 -1
- package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +1 -1
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +168 -0
- package/app/components/02.molecules/select-menu/SelectMenu.vue +384 -0
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +359 -0
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +476 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +42 -0
- package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +46 -0
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/CarouselFlip.vue +63 -42
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/stories/CarouselFlip.stories.ts +114 -29
- package/app/components/{carousels → 03.organisms/image-galleries/carousel-flip}/tests/CarouselFlip.spec.ts +80 -0
- package/app/components/05.forms/form-field/FormField.vue +1 -1
- package/app/components/05.forms/form-wrapper/FormWrapper.vue +1 -1
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +2 -7
- package/app/components/05.forms/input-button/InputButtonCore.vue +1 -5
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +26 -6
- package/app/components/05.forms/input-number/InputNumberCore.vue +125 -85
- package/app/components/05.forms/input-number/stories/InputNumberCore.stories.ts +92 -0
- package/app/components/05.forms/input-number/tests/InputNumberCore.spec.ts +119 -0
- package/app/components/05.forms/input-number/variants/InputNumberDefault.vue +30 -22
- package/app/components/05.forms/input-number/variants/stories/InputNumberDefault.stories.ts +123 -0
- package/app/components/05.forms/input-number/variants/tests/InputNumberDefault.spec.ts +143 -0
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +45 -0
- package/app/components/05.forms/input-range/InputRangeCore.vue +79 -143
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +126 -0
- package/app/components/05.forms/input-range/tests/InputRangeCore.spec.ts +124 -0
- package/app/components/05.forms/input-range/variants/InputRangeDefault.vue +9 -32
- package/app/components/05.forms/input-range/variants/stories/InputRangeDefault.stories.ts +108 -0
- package/app/components/05.forms/input-range/variants/tests/InputRangeDefault.spec.ts +140 -0
- package/app/components/05.forms/input-select/InputSelectCore.vue +23 -30
- package/app/components/05.forms/input-select/stories/InputSelectCore.stories.ts +2 -2
- package/app/components/05.forms/input-select/stories/InputSelectWithLabel.stories.ts +115 -0
- package/app/components/05.forms/input-select/tests/InputSelectCore.spec.ts +129 -0
- package/app/components/05.forms/input-select/variants/InputSelectWithLabel.vue +1 -1
- package/app/components/05.forms/input-select/variants/tests/InputSelectWithLabel.spec.ts +87 -0
- package/app/components/05.forms/input-text/tests/InputTextAsNumberWithLabel.spec.ts +14 -0
- package/app/components/05.forms/input-text/variants/InputTextAsNumberWithLabel.vue +1 -1
- package/app/components/05.forms/input-textarea/CONSUMER-STYLING.md +55 -0
- package/app/components/05.forms/input-textarea/InputTextareaCore.vue +24 -16
- package/app/components/05.forms/input-textarea/stories/InputTextareaWithLabel.stories.ts +1 -1
- package/app/components/05.forms/input-textarea/tests/InputTextareaCore.spec.ts +119 -0
- package/app/components/05.forms/input-textarea/variants/InputTextareaWithLabel.vue +3 -13
- package/app/components/05.forms/input-textarea/variants/tests/InputTextareaWithLabel.spec.ts +93 -0
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +220 -0
- package/app/components/05.forms/toggle-switch/CONSUMER-STYLING.md +29 -0
- package/app/components/05.forms/toggle-switch/ToggleSwitchCore.vue +120 -119
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchCore.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabel.stories.ts +1 -1
- package/app/components/05.forms/toggle-switch/stories/ToggleSwitchWithLabelInline.stories.ts +2 -2
- package/app/components/05.forms/toggle-switch/tests/ToggleSwitchCore.spec.ts +101 -0
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabel.vue +3 -5
- package/app/components/05.forms/toggle-switch/variants/ToggleSwitchWithLabelInline.vue +1 -2
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabel.spec.ts +77 -0
- package/app/components/05.forms/toggle-switch/variants/tests/ToggleSwitchWithLabelInline.spec.ts +66 -0
- package/app/components/05.forms/triple-toggle-switch/CONSUMER-STYLING.md +20 -6
- package/app/components/05.forms/triple-toggle-switch/TripleToggleSwitchCore.vue +12 -10
- package/app/types/components/container-glow.d.ts +8 -0
- package/app/types/components/index.ts +2 -1
- package/app/types/components/select-menu.d.ts +5 -0
- package/package.json +3 -2
- package/.vscode/srcdev-component-toggle-switch-core.code-snippets +0 -13
- package/app/components/05.forms/input-range-fancy/InputRangeFancyCore.vue +0 -426
- package/app/components/05.forms/input-range-fancy/InputRangeFancyWithLabel.vue +0 -94
- package/app/components/carousels/CarouselBasic.vue +0 -317
- package/app/components/carousels/CarouselInfinite.vue +0 -358
- package/app/components/container-glow/ContainerGlowCore.vue +0 -296
- package/app/components/container-glow/stories/ContainerGlowCore.stories.ts +0 -337
- package/app/components/display-theme-switch/DisplayThemeSwitch.vue +0 -189
- package/app/components/display-theme-switch/stories/DisplayThemeSwitch.stories.ts +0 -154
- package/app/components/glowing-border/GlowingBorder.vue +0 -142
- package/app/components/glowing-border/stories/GlowingBorder.stories.ts +0 -141
- package/app/components/layout-grids/LayoutGridB.vue +0 -134
- package/app/components/test-storybook/TestStorybook.vue +0 -49
- package/app/components/test-storybook/stories/TestStorybook.stories.ts +0 -28
- package/app/components/ui-block-decorated/UiBlockDecorated.vue +0 -167
- package/app/components/view-timeline/WipeAwayVertical.vue +0 -180
- 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
|
|
56
|
+
Same shape again:
|
|
57
57
|
|
|
58
58
|
| Token | Falls back to |
|
|
59
59
|
|---|---|
|
|
60
|
-
| `--input-button-tertiary-surface` | `
|
|
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
|
-
|
|
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
|
|
13
|
-
| `--input-number-
|
|
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-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
`
|
|
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
|
|
3
|
-
|
|
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
|
-
:
|
|
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?:
|
|
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
|
-
|
|
67
|
-
--_min-width: v-bind(minLength);
|
|
79
|
+
.input-number-wrapper {
|
|
80
|
+
--_min-width: v-bind(minLength);
|
|
68
81
|
|
|
69
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
-
|
|
76
|
-
|
|
91
|
+
display: flex;
|
|
92
|
+
align-items: center;
|
|
77
93
|
|
|
78
|
-
|
|
94
|
+
width: fit-content;
|
|
79
95
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
90
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
display: flex;
|
|
104
|
-
align-items: center;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
123
|
+
.slot {
|
|
124
|
+
display: inline-block;
|
|
107
125
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
border: none;
|
|
112
|
-
outline: none;
|
|
113
|
-
box-shadow: none;
|
|
126
|
+
.icon {
|
|
127
|
+
font-weight: 900;
|
|
128
|
+
}
|
|
114
129
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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
|
-
|
|
158
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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
|
+
});
|