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