srcdev-nuxt-components 9.6.6 → 9.6.8
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/rules/consumer/ids-from-useid.md +14 -0
- package/.claude/skills/component-compliance-checklist.md +6 -0
- package/.claude/skills/component-naming.md +2 -2
- package/.claude/skills/components/input-checkbox-radio.md +19 -3
- package/.claude/skills/components/input-checkbox.md +14 -3
- package/.claude/skills/components/input-description.md +1 -1
- package/.claude/skills/components/input-error.md +1 -1
- package/.claude/skills/components/input-label.md +2 -2
- package/.claude/skills/components/input-number.md +44 -13
- package/.claude/skills/components/input-radio.md +15 -1
- package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
- package/.claude/skills/components/input-textarea-core.md +4 -4
- package/.claude/skills/components/page-hero-highlights.md +22 -18
- package/.claude/skills/index.md +6 -6
- package/.claude/skills/testing-add-unit-test.md +17 -0
- package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
- package/.vscode/srcdev-component-input-range.code-snippets +38 -18
- package/.vscode/srcdev-component-page-hero-highlights.code-snippets +2 -1
- package/app/components/04.templates/page-hero-highlights/CONSUMER-STYLING.md +32 -12
- package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +56 -47
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +158 -307
- package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +10 -0
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +49 -27
- package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +42 -7
- package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +12 -10
- package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
- package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
- package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
- package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
- package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
- package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
- package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
- package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
- package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
- package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
- package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
- package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
- package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
- package/app/components/05.forms/input-number/InputNumber.vue +37 -25
- package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
- package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
- package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
- package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
- package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
- package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
- package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
- package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
- package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
- package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
- package/app/components/05.forms/input-range/InputRange.vue +247 -0
- package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
- package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
- package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
- package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
- package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
- package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
- package/package.json +1 -1
- package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
- package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
- package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
paths:
|
|
3
|
+
- "app/**/*.vue"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Element ids come from `useId()`, never from data
|
|
7
|
+
|
|
8
|
+
Build `id`/`for` pairs from `useId()` (plus a loop index for repeated items), never from props or
|
|
9
|
+
data values like `` `${name}-${item.value}` ``.
|
|
10
|
+
|
|
11
|
+
Why: two instances sharing a `name` and values produce duplicate ids, and a value with spaces makes
|
|
12
|
+
an invalid one, so labels toggle the wrong control (`MultipleCheckboxes`, 2026-10-16). Test
|
|
13
|
+
uniqueness with both instances inside one host component: separate `mountSuspended` calls each
|
|
14
|
+
restart the `useId()` counter. Detail: skill `testing-add-unit-test.md` (srcdev-nuxt-components).
|
|
@@ -262,6 +262,12 @@ The ledger can't see items 6c, 9, 10, 11 and 14, so check those by hand.
|
|
|
262
262
|
the edits in private memory. Note that an inline-size container can't size itself from its
|
|
263
263
|
content: say so in `CONSUMER-STYLING.md` (`ProfileSection`).
|
|
264
264
|
- Grid tracks that hold text are `minmax(0, …)` so an unbroken string can't widen the grid.
|
|
265
|
+
- An `auto-fit` column minimum taken from a token or a measured width is wrapped in
|
|
266
|
+
`min(100%, …)`, or one long item makes the column wider than its container.
|
|
267
|
+
- A component whose root is a `<fieldset>` sets `min-inline-size: 0` on it: a native fieldset
|
|
268
|
+
defaults to its min-content width and overflows a narrow column (`MultipleCheckboxes`,
|
|
269
|
+
2026-10-16).
|
|
270
|
+
- Element ids come from `useId()`, never from data values (rule `consumer/ids-from-useid.md`).
|
|
265
271
|
- Layout measured in script (connector lines, indicators) re-measures when any measured item
|
|
266
272
|
resizes or changes class, not only when the container resizes (`measured-layout-observers.md`,
|
|
267
273
|
pitfall #28).
|
|
@@ -83,8 +83,8 @@ external consumer, free to rename. Run `/check-component-usage` for the current
|
|
|
83
83
|
| InputTextWithLabel | `InputTextField` | yes |
|
|
84
84
|
| InputPasswordWithLabel | `InputPasswordField` | yes |
|
|
85
85
|
| InputTextAsNumberWithLabel | `InputTextAsNumberField` | - |
|
|
86
|
-
| InputRangeCore | `InputRange` | - |
|
|
87
|
-
| InputRangeDefault | `InputRangeField` | - |
|
|
86
|
+
| ~~InputRangeCore~~ | ✅ `InputRange` (2026-10-16) | - |
|
|
87
|
+
| ~~InputRangeDefault~~ | ✅ `InputRangeField` (2026-10-16) | - |
|
|
88
88
|
| InputTextareaCore | `InputTextarea` | - |
|
|
89
89
|
| InputTextareaWithLabel | `InputTextareaField` | yes |
|
|
90
90
|
| InputSelectCore | `InputSelect` | yes |
|
|
@@ -28,7 +28,6 @@ Use these three directly only for a custom group or layout:
|
|
|
28
28
|
|
|
29
29
|
<InputCheckboxRadioButton
|
|
30
30
|
v-for="option in options"
|
|
31
|
-
:id="`service-${option.value}`"
|
|
32
31
|
:key="option.value"
|
|
33
32
|
v-model="state.services"
|
|
34
33
|
type="checkbox"
|
|
@@ -85,9 +84,9 @@ Same props as the control minus `isButton` (always on), plus:
|
|
|
85
84
|
|
|
86
85
|
| Prop | Type | Default | Notes |
|
|
87
86
|
|---|---|---|---|
|
|
88
|
-
| `id` | `string` |
|
|
87
|
+
| `id` | `string` | generated | Used for both the input and `label[for]`. Optional since 2026-10-16: omitted, it comes from `useId()`. Never build it from data values (rule `ids-from-useid.md`). |
|
|
89
88
|
| `label` | `string` | `""` | Replaced by the `labelContent` slot when given. |
|
|
90
|
-
| `:options-layout` | `OptionsLayout` | `"equal-widths"` | `data-options-layout
|
|
89
|
+
| `:options-layout` | `OptionsLayout` | `"equal-widths"` | `data-options-layout`, a styling hook only (until 2026-10-16 `inline` made the label `nowrap`). |
|
|
91
90
|
| `direction` | `"row" \| "row-reverse"` | `"row"` | `data-direction`; `row-reverse` puts the control on the right. Reacts to changes. |
|
|
92
91
|
| `:is-pill` | `boolean` | `false` | `data-pill`, fully rounded. |
|
|
93
92
|
| `:item-icon` | `string` | `"material-symbols:add-2"` | Decorator icon name. The `itemIcon` slot replaces it entirely. |
|
|
@@ -124,6 +123,15 @@ Full detail in `app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md
|
|
|
124
123
|
|
|
125
124
|
---
|
|
126
125
|
|
|
126
|
+
## Long and hostile content
|
|
127
|
+
|
|
128
|
+
Both labels wrap long unbroken words and URLs (`overflow-wrap: anywhere`, `min-inline-size: 0` on
|
|
129
|
+
label and root), and the control and decorator icon keep their size (`flex-shrink: 0`). No
|
|
130
|
+
line-clamp tokens: an option label names the choice and must stay readable. The Field and Button
|
|
131
|
+
`StressTest` stories cover it; the bare control renders no text, so it has none.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
127
135
|
## History
|
|
128
136
|
|
|
129
137
|
**Migrated 2026-09-27** (3/5 → full compliance):
|
|
@@ -147,3 +155,11 @@ Full detail in `app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md
|
|
|
147
155
|
- Button radius, gap, paddings and the Field label colour became tokens; label colour default moved
|
|
148
156
|
from `--colour-text-default` to `--theme-text`. Removed an undeclared `--_box-shadow` and an
|
|
149
157
|
invalid unitless `min-height`.
|
|
158
|
+
|
|
159
|
+
**Stress pass 2026-10-16**:
|
|
160
|
+
|
|
161
|
+
- Removed the inline-layout `nowrap` on the button label (private `--_white-space`), which made an
|
|
162
|
+
unbroken label overflow the row; labels now wrap and the control/icon no longer shrink.
|
|
163
|
+
- `InputCheckboxRadioButton`'s `id` became optional with a `useId()` fallback.
|
|
164
|
+
- Specs now unmount every wrapper in `afterEach`; stories moved to `StoryObj` with Docs
|
|
165
|
+
descriptions, parent-story links, `CanvasSwitcher` (Field, Button) and `StressTest` stories.
|
|
@@ -24,7 +24,7 @@ instead.
|
|
|
24
24
|
|
|
25
25
|
| Prop (template form) | Type | Default | Notes |
|
|
26
26
|
|---|---|---|---|
|
|
27
|
-
| `name` | `string` | (required) | Shared `name` on every checkbox; also used
|
|
27
|
+
| `name` | `string` | (required) | Shared `name` on every checkbox; also used for the error/description ids. Button-presentation checkbox ids come from `useId()` plus the option index (until 2026-10-16 they were `${name}-${item.value}`, which duplicated across two groups sharing a name and broke on values with spaces). |
|
|
28
28
|
| `legend` | `string` | (required) | Fieldset `<legend>` text. |
|
|
29
29
|
| `:error-message` | `string \| object` | (required) | Shown by `InputError` when `fieldHasError` is true. Pass `""` when there's no error. An array renders as a list. |
|
|
30
30
|
| `:field-has-error` | `boolean` | `false` | Shows the error, sets `aria-invalid` on the fieldset and points each checkbox's `aria-describedby` at the error. |
|
|
@@ -36,7 +36,7 @@ instead.
|
|
|
36
36
|
| `:display-as-disc` | `boolean` | `false` | Round checkbox symbol. |
|
|
37
37
|
| `:theme` | `FormUiTheme` | `"default"` | Forwarded to each control. |
|
|
38
38
|
| `:input-variant` | `InputUiVariant` | `"normal"` | Forwarded to each control and the error message. |
|
|
39
|
-
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset
|
|
39
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
|
|
40
40
|
| `data-testid` | `string` | `"multiple-checkboxes"` | On the root `<fieldset>`. |
|
|
41
41
|
| `label` | `string` | `""` | **Ignored.** Kept only so existing call sites that pass it don't break; it has never rendered. Use `legend` or the description slots. |
|
|
42
42
|
|
|
@@ -84,7 +84,7 @@ instead.
|
|
|
84
84
|
| `:options-layout` | `OptionsLayout` | `"equal-widths"` | Rendered as `data-options-layout` on the items container. |
|
|
85
85
|
| `:theme` | `FormUiTheme` | `"default"` | |
|
|
86
86
|
| `:input-variant` | `InputUiVariant` | `"normal"` | |
|
|
87
|
-
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset
|
|
87
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
|
|
88
88
|
| `data-testid` | `string` | `"single-checkbox"` | On the root `<fieldset>`. |
|
|
89
89
|
|
|
90
90
|
### v-model
|
|
@@ -122,6 +122,17 @@ controls' own colours are the `--input-checkbox-*` tokens in `input-checkbox-rad
|
|
|
122
122
|
|
|
123
123
|
---
|
|
124
124
|
|
|
125
|
+
## Long and hostile content
|
|
126
|
+
|
|
127
|
+
Both root fieldsets set `min-inline-size: 0` (a native `<fieldset>` otherwise grows to its
|
|
128
|
+
min-content width and overflows a narrow column) and `overflow-wrap: anywhere`, which inherits
|
|
129
|
+
into the legend, description, labels and error, so long unbroken words and URLs wrap. Equal-width
|
|
130
|
+
columns are capped at `100%` of the container. Button-mode labels wrap too (`InputCheckboxRadioButton` dropped its inline `nowrap`
|
|
131
|
+
2026-10-16). No line-clamp tokens: the legend, option labels and error name the choice or the
|
|
132
|
+
problem and must stay readable. The `StressTest` story in each story file covers this.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
125
136
|
## Migration notes (2026-09-25)
|
|
126
137
|
|
|
127
138
|
Brought to full compliance. Behaviour changes a consumer could notice:
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`InputDescription` renders a form field's help text. Every `*Field` wrapper in `05.forms`
|
|
6
6
|
(`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
|
|
7
|
-
`
|
|
7
|
+
`InputRangeField`, `InputTextAsNumberWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
|
|
8
8
|
`MultipleRadiobuttons`) already renders one and forwards its own `descriptionText` /
|
|
9
9
|
`descriptionHtml` slots into it, so consumers normally just pass those slots to the wrapper:
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`InputError` renders the red error message strip under a form control. Every `*Field` wrapper in
|
|
6
6
|
`05.forms` (`InputTextWithLabel`, `InputNumberField`, `InputSelectWithLabel`, `InputTextareaWithLabel`,
|
|
7
|
-
`
|
|
7
|
+
`InputRangeField`, `ToggleSwitchWithLabel`, `SingleCheckbox`, `MultipleCheckboxes`,
|
|
8
8
|
`MultipleRadiobuttons`, `InputTextAsNumberWithLabel`) already renders one, so consumers rarely use
|
|
9
9
|
it directly. Use it yourself only when building a custom field wrapper.
|
|
10
10
|
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
`InputLabel` renders a form field's `<label>`. Every labelled wrapper in `05.forms`
|
|
6
6
|
(`InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
|
|
7
|
-
`InputTextareaWithLabel`, `
|
|
7
|
+
`InputTextareaWithLabel`, `InputRangeField`, `ToggleSwitchWithLabel`,
|
|
8
8
|
`ToggleSwitchWithLabelInline`) already renders one from its `label` prop, so consumers normally
|
|
9
9
|
never use it directly.
|
|
10
10
|
|
|
@@ -62,7 +62,7 @@ export default defineAppConfig({
|
|
|
62
62
|
- `"required"` mode: a marker on fields with `required`; `"optional"` mode: a marker on fields
|
|
63
63
|
without it; `"none"`: nothing (default, so existing sites are unchanged).
|
|
64
64
|
- `InputTextWithLabel`, `InputTextAsNumberWithLabel`, `InputNumberField`, `InputSelectWithLabel`,
|
|
65
|
-
`InputTextareaWithLabel` and `
|
|
65
|
+
`InputTextareaWithLabel` and `InputRangeField` forward `:required`.
|
|
66
66
|
`ToggleSwitchWithLabel`/`ToggleSwitchWithLabelInline` pass `indicator="none"`: a switch always
|
|
67
67
|
has a value, so neither marker means anything there.
|
|
68
68
|
- The wrappers don't expose the indicator props per field; use `app.config`.
|
|
@@ -29,17 +29,17 @@ Most consumers should reach for **InputNumberField** rather than `InputNumber` d
|
|
|
29
29
|
| `:placeholder` | `string` | `""` | Native `placeholder`. |
|
|
30
30
|
| `:required` | `boolean` | `false` | Native `required`. |
|
|
31
31
|
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
32
|
-
| `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `
|
|
33
|
-
| `:input-variant` | `InputUiVariant` | `"normal"` |
|
|
32
|
+
| `:weight` | `FormWeight` | `"normal"` | Adds an `input-number--{weight}` class to the input, for prop-shape consistency with sibling `05.forms` components. **No CSS in the library currently styles this class — it's a no-op today**, same convention as `InputRange`'s `weight` prop. |
|
|
33
|
+
| `:input-variant` | `InputUiVariant` | `"normal"` | Rendered as `data-input-variant` on the wrapper (a bare class until 2026-10-16) — `"normal"` (bordered box) and `"underlined"` (bottom-border only) have CSS; `"outlined"` doesn't (see CONSUMER-STYLING.md). Same prop/behaviour as `InputTextCore`. |
|
|
34
34
|
| `:field-has-error` | `boolean` | `false` | Drives `data-invalid` on the wrapper. |
|
|
35
|
-
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
|
|
35
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. Reacts to changes. |
|
|
36
36
|
| `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
|
|
37
37
|
|
|
38
38
|
### v-model
|
|
39
39
|
|
|
40
40
|
`v-model` — `number | readonly number[]`, **required** (`defineModel({ required: true })`). Bound
|
|
41
41
|
directly to the native number input. Required rather than defaulted deliberately — same reasoning
|
|
42
|
-
as `
|
|
42
|
+
as `InputRange`'s `v-model` (see that skill doc) — this also satisfies `vue/require-default-prop`.
|
|
43
43
|
|
|
44
44
|
---
|
|
45
45
|
|
|
@@ -47,15 +47,15 @@ as `InputRangeCore`'s `v-model` (see that skill doc) — this also satisfies `vu
|
|
|
47
47
|
|
|
48
48
|
| Slot | Notes |
|
|
49
49
|
|------|-------|
|
|
50
|
-
| `left` | Rendered before the input (e.g. a step-down button)
|
|
51
|
-
| `right` | Rendered after the input (e.g. a step-up button)
|
|
50
|
+
| `left` | Rendered before the input (e.g. a step-down button) in `.input-number-slot-left`. Sets `data-has-left-slot` on the wrapper. |
|
|
51
|
+
| `right` | Rendered after the input (e.g. a step-up button) in `.input-number-slot-right`. Sets `data-has-right-slot` on the wrapper. |
|
|
52
52
|
|
|
53
|
-
Any `.input-button` inside `.slot` gets its own border/outline stripped and is sized to
|
|
53
|
+
Any `.input-button` inside `.input-number-slot` gets its own border/outline stripped and is sized to
|
|
54
54
|
match the input (`aspect-ratio: 1`, `width: var(--input-min-height)`) regardless of which slot
|
|
55
55
|
it's in. The divider border is then added directly to that button element specifically when its
|
|
56
|
-
slot is present
|
|
57
|
-
right-hand mirror
|
|
58
|
-
containing `.slot` div. This makes step buttons read as embedded segments of the same control
|
|
56
|
+
slot is present (`.input-number-slot-left .input-button { border-right: ... }` and the
|
|
57
|
+
right-hand mirror), so it sits flush against the button's own edge rather than around the
|
|
58
|
+
containing `.input-number-slot` div. This makes step buttons read as embedded segments of the same control
|
|
59
59
|
rather than separate floating buttons. The divider reuses the wrapper's own border colour token
|
|
60
60
|
(no separate divider token), same as `InputTextCore`/`InputTextAsNumberWithLabel`.
|
|
61
61
|
|
|
@@ -83,7 +83,7 @@ score-1/5 state. Several latent bugs were fixed alongside the standard checklist
|
|
|
83
83
|
- The native `<input>` had no static `input-number` class — only the dynamic
|
|
84
84
|
`elementClasses` passthrough — so the entire `.input-number { ... }` style block (font,
|
|
85
85
|
padding, colour, focus box-shadow) never matched anything. Fixed by adding the class directly.
|
|
86
|
-
- `placeholder` was declared as a prop but never bound on the `<input>` — unlike `
|
|
86
|
+
- `placeholder` was declared as a prop but never bound on the `<input>` — unlike `InputRange`
|
|
87
87
|
(where a range input genuinely has no placeholder concept and the no-op is intentional and
|
|
88
88
|
documented), a number input *does* support `placeholder` meaningfully, so this was a real gap,
|
|
89
89
|
not a deliberate no-op. Fixed by binding `:placeholder`.
|
|
@@ -107,9 +107,9 @@ score-1/5 state. Several latent bugs were fixed alongside the standard checklist
|
|
|
107
107
|
aligned with the rest of `05.forms`.
|
|
108
108
|
- `weight` was passed from `InputNumberField` to `InputNumber` but `InputNumber` had no
|
|
109
109
|
matching prop, so it fell through as a raw, meaningless `weight="normal"` HTML attribute on the
|
|
110
|
-
wrapper `<div>`. Fixed by declaring the prop on `InputNumber` too, matching `
|
|
110
|
+
wrapper `<div>`. Fixed by declaring the prop on `InputNumber` too, matching `InputRange`.
|
|
111
111
|
- `defineModel()` had no default, tripping `vue/require-default-prop` — fixed the same way as
|
|
112
|
-
`
|
|
112
|
+
`InputRange`: `defineModel({ required: true })`, not a rule disable or an artificial default.
|
|
113
113
|
- A single-use private token, `--_border`, was inlined to the public
|
|
114
114
|
`var(--input-number-border, var(--theme-border))` directly at its point of use — it had no
|
|
115
115
|
composition or state-swap behind it, just a bare pass-through (see pitfall #20). `--_surface`
|
|
@@ -221,3 +221,34 @@ button icon content itself is consumer-supplied).
|
|
|
221
221
|
|
|
222
222
|
Step-down is disabled (`readonly` on its `InputButton`) once the value reaches `min`, and
|
|
223
223
|
step-up once it reaches `max`; clicking either button while at that boundary is a no-op.
|
|
224
|
+
Each press adds or subtracts `step` (its absolute value, or 1 when it's 0), rounds away
|
|
225
|
+
floating-point drift (0.2 + 0.1 gives 0.3) and clamps to `min`/`max`, so a step that would overshoot
|
|
226
|
+
lands on the limit and a typed out-of-range value is pulled back in. From an empty field the first
|
|
227
|
+
press sets 0, clamped into range. `:style-class-passthrough` lands on the root `.input-number-field`
|
|
228
|
+
only (it also reached the inner input until 2026-10-16) and reacts to changes. The root carries
|
|
229
|
+
`data-input-variant`, `data-theme` and `data-invalid`; its old `error`, `theme-*` and variant
|
|
230
|
+
classes are gone.
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## Stress pass 2026-10-16
|
|
235
|
+
|
|
236
|
+
- **Stepping** (`InputNumberField`): 0.1 steps drifted (0.30000000000000004); a step that didn't
|
|
237
|
+
divide the range overshot `max`/`min` (8 + 5 with max 10 gave 13); an empty field turned into
|
|
238
|
+
`NaN`. Now rounded to the step's precision, clamped to `min`/`max`, and an empty field starts at
|
|
239
|
+
0, clamped. A zero or negative `step` steps by its size, or 1.
|
|
240
|
+
- **Mobile keyboard**: `inputmode="numeric"` and `pattern="[0-9]+"` were hardcoded, so a negative
|
|
241
|
+
or decimal value couldn't be typed on phones (and number inputs ignore `pattern`). `inputmode` is
|
|
242
|
+
now `numeric` for whole steps, `decimal` for fractional ones, and omitted when `min` is negative;
|
|
243
|
+
`pattern` is gone.
|
|
244
|
+
- **Width**: the input's minimum width was `max`'s digit count in `em`, uncapped and blind to a
|
|
245
|
+
longer negative `min` or the step's decimals. It now counts the longest of `min`/`max` plus the
|
|
246
|
+
step's decimals, capped at `12em`; the wrapper has `max-inline-size: 100%` and the step buttons
|
|
247
|
+
don't shrink. The Field wraps long label, description and error copy.
|
|
248
|
+
- **State hooks**: bare state classes became data attributes and the slot holders got prefixed
|
|
249
|
+
class names (CONSUMER-STYLING.md has the mapping). Removed the non-existent `body-normal-bold`
|
|
250
|
+
utility class from the label and a dead `.label-description` rule. `InputError` now gets
|
|
251
|
+
`input-variant` like the label does.
|
|
252
|
+
- Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
|
|
253
|
+
`DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
|
|
254
|
+
control's step-button demo uses `InputButton`, not raw buttons.
|
|
@@ -41,7 +41,7 @@ option in `fieldData.data`, and an `InputError`. It's the single-choice sibling
|
|
|
41
41
|
| `:options-layout` | `OptionsLayout` | `"equal-widths"` | `"equal-widths"` (auto-fit grid sized to the longest label), `"inline"` (wrapping row) or `"block"` (column). Rendered as `data-options-layout` on the items container. |
|
|
42
42
|
| `:theme` | `FormUiTheme` | `"default"` | Passed to each radio. |
|
|
43
43
|
| `:input-variant` | `InputUiVariant` | `"normal"` | Passed to each radio, the description and the error. |
|
|
44
|
-
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset
|
|
44
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Classes on the root `<fieldset>`; updates when the prop changes. |
|
|
45
45
|
| `data-testid` | `string` | `"multiple-radio-buttons"` | On the root `<fieldset>`. |
|
|
46
46
|
|
|
47
47
|
### v-model
|
|
@@ -93,3 +93,17 @@ tokens from `input-checkbox-radio`.
|
|
|
93
93
|
to `data-options-layout`; `MultipleCheckboxes` and `SingleCheckbox` got the same change.
|
|
94
94
|
- Gap and top margin are public tokens. The label-width measurement no longer includes a
|
|
95
95
|
selector for a class that doesn't exist. `fieldData` is a typed required model.
|
|
96
|
+
|
|
97
|
+
**Stress pass 2026-10-16**:
|
|
98
|
+
|
|
99
|
+
- Button-mode radio ids were `${name}-${item.value}`, so two groups sharing a `name` got duplicate
|
|
100
|
+
ids and a value with spaces made an invalid one (CLAUDE.md pitfall 30). `MultipleRadiobuttons`
|
|
101
|
+
no longer passes an `id`; `InputCheckboxRadioButton` generates one with `useId()`.
|
|
102
|
+
- The root fieldset sets `min-inline-size: 0` and `overflow-wrap: anywhere`, and equal-width
|
|
103
|
+
columns are capped at `min(100%, ...)`, so long legends, labels and errors wrap inside a narrow
|
|
104
|
+
column (same fix as `MultipleCheckboxes`). No line-clamp tokens: the legend, option labels and
|
|
105
|
+
error must stay readable.
|
|
106
|
+
- `styleClassPassthrough` is watched instead of read once at mount.
|
|
107
|
+
- The spec unmounts every wrapper in `afterEach`; the story moved to `StoryObj` with Docs
|
|
108
|
+
descriptions, `CanvasSwitcher` and a `StressTest` story, and its gap control is labelled as a
|
|
109
|
+
story-only CSS token.
|
|
@@ -1,15 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# InputRange Component
|
|
2
|
+
|
|
3
|
+
> **Renamed 2026-10-16**: `InputRangeCore` → `InputRange`, `InputRangeDefault` → `InputRangeField`
|
|
4
|
+
> (DOM classes `.input-range-core` → `.input-range`, `.input-range-with-label` →
|
|
5
|
+
> `.input-range-field`; full class list in CONSUMER-STYLING.md). Skill doc renamed from
|
|
6
|
+
> `input-range-core.md`. No consumer app used either old name.
|
|
2
7
|
|
|
3
8
|
## Overview
|
|
4
9
|
|
|
5
|
-
`
|
|
10
|
+
`InputRange` is the low-level native `<input type="range">` primitive for the `05.forms`
|
|
6
11
|
range-slider family. It renders a single native range input with left/right button slots (for
|
|
7
12
|
step-down/step-up controls), an optional `markers` slot for custom tick marks overlaid on the
|
|
8
13
|
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 `
|
|
14
|
+
description, or error-message rendering of its own — that's composed by the `InputRangeField`
|
|
10
15
|
variant below.
|
|
11
16
|
|
|
12
|
-
Most consumers should reach for **
|
|
17
|
+
Most consumers should reach for **InputRangeField** rather than `InputRange` directly.
|
|
13
18
|
|
|
14
19
|
---
|
|
15
20
|
|
|
@@ -20,7 +25,7 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
|
|
|
20
25
|
| Prop (template form) | Type | Default | Notes |
|
|
21
26
|
|------|------|---------|-------|
|
|
22
27
|
| `:id` | `string` | (required) | Applied to the native input. |
|
|
23
|
-
| `:name` | `string` | (required) | Applied to the native input
|
|
28
|
+
| `:name` | `string` | (required) | Applied to the native input. (Until 2026-10-16 it also built the datalist id, `${name}-datalist`; that id now comes from `useId()`.) |
|
|
24
29
|
| `:min` | `number` | (required) | Native `min`. |
|
|
25
30
|
| `:max` | `number` | (required) | Native `max`. |
|
|
26
31
|
| `:step` | `number` | `1` | Native `step`. |
|
|
@@ -28,8 +33,8 @@ Most consumers should reach for **InputRangeDefault** rather than `InputRangeCor
|
|
|
28
33
|
| `:placeholder` | `string` | `""` | Declared but not rendered — range inputs have no placeholder concept. Kept for prop-shape consistency with sibling `05.forms` components. |
|
|
29
34
|
| `:theme` | `FormUiTheme` | `"default"` | Sets `data-theme` on the wrapper. |
|
|
30
35
|
| `: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` |
|
|
32
|
-
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. |
|
|
36
|
+
| `:field-has-error` | `boolean` | `false` | `data-invalid` on the wrapper and `aria-invalid="true"` on the input (it did nothing here until 2026-10-16). |
|
|
37
|
+
| `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the input element. Reacts to changes. |
|
|
33
38
|
| `:aria-describedby` | `string` | `""` | Forwarded to the native input. |
|
|
34
39
|
|
|
35
40
|
### v-model
|
|
@@ -50,8 +55,8 @@ disable, not a fake default).
|
|
|
50
55
|
|------|-------|
|
|
51
56
|
| `left` | Rendered before the track (e.g. a step-down button). |
|
|
52
57
|
| `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).
|
|
54
|
-
| `datalist` | Rendered after the input
|
|
58
|
+
| `markers` | Rendered as an overlay on top of the track (`grid-area: element-stack`, same cell as the input). Sets `data-has-markers` on the wrapper: a line joining the markers is drawn behind them, the input becomes a transparent layer over the whole marker row (so the row is draggable and clickable), and the thumb is restyled as a ring the size of one marker that circles the selected one. That lines up because markers are spaced `space-between` and given an explicit size (icon size + 2 × padding), the same as the thumb. Use one marker per step (`(max - min) / step + 1`). Expected shape: a `.input-range-markers` wrapper containing `.input-range-marker` elements (each optionally containing an `.input-range-marker-icon`); those classes are pre-styled (they were `.marker`/`.marker-icon` until 2026-10-16). |
|
|
59
|
+
| `datalist` | Rendered after the input, with a `datalistId` slot prop: put it on your `<datalist>` (`<template #datalist="{ datalistId }"><datalist :id="datalistId">`) and the input's `list` points at it. The id comes from `useId()`, so two ranges sharing a `name` don't collide. `list` is omitted when the slot is empty. |
|
|
55
60
|
|
|
56
61
|
---
|
|
57
62
|
|
|
@@ -93,13 +98,13 @@ was itself commented out.
|
|
|
93
98
|
|
|
94
99
|
## Variants
|
|
95
100
|
|
|
96
|
-
###
|
|
101
|
+
### InputRangeField
|
|
97
102
|
|
|
98
|
-
`
|
|
103
|
+
`InputRangeField` (`InputRangeField.vue`) composes `InputRange` with `InputLabel`,
|
|
99
104
|
`InputDescription`, and `InputError`, plus optional step-down/step-up buttons (`InputButton`)
|
|
100
105
|
wired into the `left`/`right` slots.
|
|
101
106
|
|
|
102
|
-
**Additional props over
|
|
107
|
+
**Additional props over InputRange:**
|
|
103
108
|
|
|
104
109
|
| Prop (template form) | Type | Default | Notes |
|
|
105
110
|
|------|------|---------|-------|
|
|
@@ -110,11 +115,11 @@ wired into the `left`/`right` slots.
|
|
|
110
115
|
|
|
111
116
|
**Slots**: `descriptionHtml`, `descriptionText` (both forwarded to `InputDescription`), plus
|
|
112
117
|
`left`/`right`/`markers`/`datalist` (forwarded straight through to the underlying
|
|
113
|
-
`
|
|
118
|
+
`InputRange`; `markers` wasn't forwarded until 2026-10-16, and `datalist` passes `datalistId` on — the step-down/step-up buttons only render when you use the `left`/`right`
|
|
114
119
|
slots yourself, since the button icon content itself is consumer-supplied via `#iconOnly`).
|
|
115
120
|
|
|
116
121
|
```vue
|
|
117
|
-
<
|
|
122
|
+
<InputRangeField
|
|
118
123
|
name="priceRange"
|
|
119
124
|
label="Price range"
|
|
120
125
|
v-model="price"
|
|
@@ -126,17 +131,47 @@ slots yourself, since the button icon content itself is consumer-supplied via `#
|
|
|
126
131
|
>
|
|
127
132
|
<template #left><Icon name="mdi:minus" /></template>
|
|
128
133
|
<template #right><Icon name="mdi:plus" /></template>
|
|
129
|
-
</
|
|
134
|
+
</InputRangeField>
|
|
130
135
|
```
|
|
131
136
|
|
|
132
|
-
`v-model` is `number`, **required** — same as `
|
|
137
|
+
`v-model` is `number`, **required** — same as `InputRange` above.
|
|
138
|
+
|
|
139
|
+
Each step-button press adds or subtracts `step` (its absolute value, or 1 when it's 0), rounds away
|
|
140
|
+
floating-point drift (0.2 + 0.1 gives 0.3) and clamps to `min`/`max`; step-down goes read-only at or
|
|
141
|
+
below `min`, step-up at or above `max`. `:style-class-passthrough` lands on the root
|
|
142
|
+
`.input-range-field` only (it also reached the inner input until 2026-10-16) and reacts to changes.
|
|
143
|
+
The root carries `data-theme` and `data-invalid` (its old `error` class is gone), and the label
|
|
144
|
+
gets `input-range-label` (it was the number field's `input-number-label`, with a non-existent
|
|
145
|
+
`body-normal-bold` utility).
|
|
133
146
|
|
|
134
147
|
**Fixed 2026-09-21**: removed a dead `deepCssClassPassthrough` prop (declared, never consumed
|
|
135
148
|
anywhere in the component or referenced elsewhere in the codebase) and a dead `<style>` block
|
|
136
149
|
targeting `.input-range-label`/`.label-description` — classes that nothing in this component (or
|
|
137
150
|
`InputLabel`/`InputDescription`) actually renders, so the block had no effect. Also removed two
|
|
138
151
|
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 `
|
|
140
|
-
`
|
|
152
|
+
(same day, follow-up pass) a `const props = withDefaults(...)` in both `InputRange` and
|
|
153
|
+
`InputRangeField` that had become entirely unused once the dead `FormUiTheme` computed was
|
|
141
154
|
removed — the template's `:id`/`:name`/etc. shorthand bindings resolve straight from `defineProps`
|
|
142
155
|
automatically in `<script setup>`, no destructuring or `props.` reference needed.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Stress pass 2026-10-16
|
|
160
|
+
|
|
161
|
+
- **Stepping** (`InputRangeField`): 0.1 steps drifted (0.30000000000000004) and a step that didn't
|
|
162
|
+
divide the range overshot `min`/`max`; the read-only checks used `===`, so an overshot value
|
|
163
|
+
never disabled the button. Now rounded, clamped, and `<=`/`>=`. A zero or negative `step`
|
|
164
|
+
steps by its size, or 1.
|
|
165
|
+
- **Datalist id**: was `${name}-datalist`, so two ranges sharing a `name` linked to the same list
|
|
166
|
+
(rule `ids-from-useid.md`). Now `useId()`, handed to the slot as `datalistId`.
|
|
167
|
+
- **Error state**: `fieldHasError` did nothing on `InputRange`; it now sets `data-invalid` and
|
|
168
|
+
`aria-invalid`.
|
|
169
|
+
- **Slots**: the Field didn't forward `markers`.
|
|
170
|
+
- **Classes**: bare/generic classes became prefixed classes or data attributes, and the marker
|
|
171
|
+
defaults moved from dark-page colours to `--theme-*` tokens (CONSUMER-STYLING.md has the
|
|
172
|
+
mapping). The Field's passthrough no longer reaches the inner input as well as its root.
|
|
173
|
+
- **Overflow**: the slider container gets `min-inline-size: 0` and the step buttons don't
|
|
174
|
+
shrink; the Field wraps long label, description and error copy.
|
|
175
|
+
- Specs unmount in `afterEach`; stories moved to `StoryObj` with Docs descriptions, a
|
|
176
|
+
`DecimalSteps` story, `CanvasSwitcher` and a `StressTest` story on the Field. The bare
|
|
177
|
+
control's step-button demo uses `InputButton`, not raw buttons.
|
|
@@ -33,7 +33,7 @@ directly.
|
|
|
33
33
|
|
|
34
34
|
- `v-model` — `string | number | readonly string[] | null | undefined`, **required**
|
|
35
35
|
(`defineModel({ required: true })`, satisfying `vue/require-default-prop` the same way as
|
|
36
|
-
`
|
|
36
|
+
`InputRange`/`InputNumber` — see `project_ledger_eslint_issues_column` in memory).
|
|
37
37
|
"Required" here means a consumer must bind `v-model`, not that the value can't be an empty
|
|
38
38
|
string — `""` is a perfectly valid textarea value.
|
|
39
39
|
- `v-model:is-dirty` — `boolean`. Set once by `InputTextareaWithLabel` the first time the value
|
|
@@ -50,7 +50,7 @@ directly.
|
|
|
50
50
|
| `left` | Rendered before the textarea (decorative — e.g. an icon). Adds `has-left-slot` to the wrapper. |
|
|
51
51
|
| `right` | Rendered after the textarea (decorative). Adds `has-right-slot` to the wrapper. |
|
|
52
52
|
|
|
53
|
-
Unlike `InputNumber`/`
|
|
53
|
+
Unlike `InputNumber`/`InputRange`, these slots have no built-in `InputButton`
|
|
54
54
|
divider/embedded-button styling — they're plain decorative content (see the Storybook stories,
|
|
55
55
|
which use emoji), not a stepper control.
|
|
56
56
|
|
|
@@ -75,7 +75,7 @@ the full table with defaults.
|
|
|
75
75
|
score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill doc, no snippet).
|
|
76
76
|
|
|
77
77
|
- **Silently broken styling from undefined CSS custom properties** (same bug class as pitfall #21
|
|
78
|
-
and `
|
|
78
|
+
and `InputRange`'s dead `--theme-form-range-accent-color`): the wrapper's `padding-inline`
|
|
79
79
|
read `--element-decorator-padding-inline`, and the placeholder's `font-size`/`font-style`/
|
|
80
80
|
`line-height`/`font-weight` read `--input-placeholder-font-size`/`-font-style`/`-line-height`/
|
|
81
81
|
`-font-weight` — **none of these five custom properties were declared anywhere in the
|
|
@@ -105,7 +105,7 @@ score-1/5 state (no public tokens, no CONSUMER-STYLING.md, no tests, no skill do
|
|
|
105
105
|
`InputLabel` (which does have a real `label` concept) already renders it correctly. Covered by
|
|
106
106
|
a regression test.
|
|
107
107
|
- **`defineModel()` had no default**, tripping `vue/require-default-prop` in both files — fixed
|
|
108
|
-
with `{ required: true }`, same pattern as `
|
|
108
|
+
with `{ required: true }`, same pattern as `InputRange`/`InputNumber` (see the v-model
|
|
109
109
|
note above for why this is still correct even though an empty string is a valid value here).
|
|
110
110
|
- **`InputTextareaWithLabel.stories.ts` used `reactive()` in two render functions
|
|
111
111
|
(`AllVariants`/`FormExamples`) without importing it** — only `computed` was imported from
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# PageHeroHighlights Component
|
|
2
2
|
|
|
3
|
+
> **Changed 2026-10-16:** `widthConstrained` was inverted in the CSS since it was first added (`true` gave full width, `false` capped the column); it now does what this doc always said. The tablet/desktop gutters never applied (their `@container` queries had no container): the root is now a named `page-hero-highlights` inline-size container and the rows sit in a `.page-hero-highlights-grid` wrapper. Colour defaults are light theme tokens (were `darkblue`/`white`/`black`/`--slate-*`). Equal-width highlights wrap below `--page-hero-highlights-item-min-width` (12rem). Grid tracks are `minmax(0, …)` and items get `overflow-wrap: anywhere`.
|
|
4
|
+
|
|
3
5
|
> **Changed 2026-09-28:** styles are now in `@layer components` (they were unlayered). Every token is `--page-hero-highlights-*` with its default as a `var()` fallback, so it can be set on the component or any ancestor (previously declared on the root with generic names like `--max-width`, `--highlight-border`, so ancestor values never landed). Inner classes are prefixed (`.page-hero-highlights-header-row`, `-header-slot`, `-strip`, `-content-row`, `-content-slot`); the content-align root classes are `.content-align-start`/`.content-align-center` (were bare `.start`/`.center`); the highlight contract classes are `.page-hero-highlights-item`, `-item-title`, `-item-body` (were `.highlight`, `.title`, `.body`); PageHeroHighlightsHeader uses `.page-hero-highlights-header-start`/`-end` and `--page-hero-highlights-header-*` tokens (were `.phh-*` / `--phh-*`). Full token reference: `CONSUMER-STYLING.md` in the component folder.
|
|
4
6
|
|
|
5
7
|
## Overview
|
|
@@ -19,7 +21,7 @@ The layout uses a 4-row CSS Grid with `subgrid` — no `translate`, negative mar
|
|
|
19
21
|
| `tag` | `"div" \| "section" \| "main"` | `"div"` | Root element tag |
|
|
20
22
|
| `highlightsEqualWidths` | `boolean` | `false` | Equal-width grid columns for highlight items |
|
|
21
23
|
| `highlightsJustify` | `"start" \| "center" \| "end" \| "space-between" \| "space-around"` | `"start"` | Alignment of highlight items along the main axis |
|
|
22
|
-
| `widthConstrained` | `boolean` | `false` | When `true`, caps the central column at `--width
|
|
24
|
+
| `widthConstrained` | `boolean` | `false` | When `true`, caps the central column at `--page-hero-highlights-max-width` (default `1064px`). Gutters grow responsively to enforce the constraint. |
|
|
23
25
|
| `contentAlign` | `"start" \| "center"` | `"center"` | When `widthConstrained` is `true`: `"center"` grows gutters equally; `"start"` pins content to the left with a fixed left gutter. |
|
|
24
26
|
| `contentPanel` | `boolean` | `true` | When `true`, renders a decorative panel behind the content slot and offsets the highlights strip. Set to `false` for a flat layout with no backdrop. |
|
|
25
27
|
| `highlightTitleBaseline`| `boolean` | `false` | When `true`, fixes the highlight title row to a set height so titles align at a common baseline. Override `--page-hero-highlights-title-height` to tune. |
|
|
@@ -166,7 +168,7 @@ See [component-aria-landmark.md](../component-aria-landmark.md) for the full pat
|
|
|
166
168
|
|
|
167
169
|
## Equal-width highlights
|
|
168
170
|
|
|
169
|
-
By default, highlight items size to their content (`flex-wrap`). Pass `:highlights-equal-widths="true"` to switch to a grid where all items share equal column widths:
|
|
171
|
+
By default, highlight items size to their content (`flex-wrap`). Pass `:highlights-equal-widths="true"` to switch to a grid where all items share equal column widths (`repeat(auto-fit, minmax(min(100%, --page-hero-highlights-item-min-width), 1fr))`), wrapping onto new rows when they'd get narrower than the min width (default `12rem`):
|
|
170
172
|
|
|
171
173
|
```vue
|
|
172
174
|
<PageHeroHighlights :highlights-equal-widths="true">
|
|
@@ -176,10 +178,10 @@ By default, highlight items size to their content (`flex-wrap`). Pass `:highligh
|
|
|
176
178
|
|
|
177
179
|
## Constraining the central column width
|
|
178
180
|
|
|
179
|
-
Pass `:width-constrained="true"` to cap the content column at `--width
|
|
181
|
+
Pass `:width-constrained="true"` to cap the content column at `--page-hero-highlights-max-width` (default `1064px`). The gutters grow responsively to enforce it — full-bleed backgrounds are unaffected and `subgrid` continues to work. Use `content-align` to pin to the left or centre:
|
|
180
182
|
|
|
181
183
|
```vue
|
|
182
|
-
<!-- Centred, capped at --width
|
|
184
|
+
<!-- Centred, capped at --page-hero-highlights-max-width (1064px) -->
|
|
183
185
|
<PageHeroHighlights :width-constrained="true" content-align="center">...</PageHeroHighlights>
|
|
184
186
|
|
|
185
187
|
<!-- Left-pinned (right side takes remaining space) -->
|
|
@@ -190,12 +192,12 @@ The maximum width value and gutter sizes are all CSS tokens — override them vi
|
|
|
190
192
|
|
|
191
193
|
```css
|
|
192
194
|
.my-page-hero {
|
|
193
|
-
--width
|
|
195
|
+
--page-hero-highlights-max-width: 1200px;
|
|
194
196
|
--page-hero-highlights-gutter-desktop: 48px;
|
|
195
197
|
}
|
|
196
198
|
```
|
|
197
199
|
|
|
198
|
-
See [css-grid-width-
|
|
200
|
+
See [css-grid-max-width-gutters.md](../css-grid-max-width-gutters.md) for the underlying pattern explanation.
|
|
199
201
|
|
|
200
202
|
## Local style override scaffold
|
|
201
203
|
|
|
@@ -215,40 +217,41 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
|
|
|
215
217
|
.page-hero-highlights {
|
|
216
218
|
&.my-page-hero {
|
|
217
219
|
/* Grid layout */
|
|
218
|
-
/* --width
|
|
220
|
+
/* --page-hero-highlights-max-width: 1064px; */
|
|
219
221
|
/* --page-hero-highlights-gutter-mobile: 16px; */
|
|
220
222
|
/* --page-hero-highlights-gutter-tablet: 40px; */
|
|
221
223
|
/* --page-hero-highlights-gutter-desktop: 32px; */
|
|
222
224
|
|
|
223
225
|
/* Header zone */
|
|
224
|
-
/* --page-hero-highlights-header-background:
|
|
226
|
+
/* --page-hero-highlights-header-background: var(--theme-surface-subtle); */
|
|
225
227
|
|
|
226
228
|
/* Highlights strip */
|
|
227
229
|
/* --page-hero-highlights-strip-gap: 1rem; */
|
|
228
230
|
/* --page-hero-highlights-strip-inset: 1.2rem; */
|
|
231
|
+
/* --page-hero-highlights-item-min-width: 12rem; */ /* equal-widths wrap point */
|
|
229
232
|
|
|
230
233
|
/* Highlight cards */
|
|
231
234
|
/* --page-hero-highlights-item-rows-gap: 1.2rem; */
|
|
232
235
|
/* --page-hero-highlights-title-height: 1fr; */ /* see: highlight-title-baseline prop */
|
|
233
236
|
/* --page-hero-highlights-item-padding-block-start: 1.2rem; */
|
|
234
237
|
/* --page-hero-highlights-item-padding: 1.2rem; */
|
|
235
|
-
/* --page-hero-highlights-item-background:
|
|
236
|
-
/* --page-hero-highlights-item-border: 1px solid
|
|
238
|
+
/* --page-hero-highlights-item-background: var(--page-bg, Canvas); */
|
|
239
|
+
/* --page-hero-highlights-item-border: 1px solid var(--theme-border); */
|
|
237
240
|
/* --page-hero-highlights-item-border-radius: 8px; */
|
|
238
|
-
/* --page-hero-highlights-item-colour:
|
|
241
|
+
/* --page-hero-highlights-item-colour: var(--theme-text); */
|
|
239
242
|
|
|
240
243
|
/* Content zone */
|
|
241
|
-
/* --page-hero-highlights-content-background:
|
|
244
|
+
/* --page-hero-highlights-content-background: transparent; */
|
|
242
245
|
/* --page-hero-highlights-content-start-gap: 1.2rem; */
|
|
243
246
|
/* --page-hero-highlights-content-end-gap: 1.2rem; */
|
|
244
247
|
|
|
245
248
|
/* Content slot decorative border */
|
|
246
249
|
/* --page-hero-highlights-content-margin-block-start: 2.4rem; */
|
|
247
250
|
/* --page-hero-highlights-content-margin: var(--page-hero-highlights-strip-inset); */
|
|
248
|
-
/* --page-hero-highlights-panel-background: var(--
|
|
249
|
-
/* --page-hero-highlights-panel-border: 1px solid var(--
|
|
251
|
+
/* --page-hero-highlights-panel-background: var(--theme-surface-subtle); */
|
|
252
|
+
/* --page-hero-highlights-panel-border: 1px solid var(--theme-border); */
|
|
250
253
|
/* --page-hero-highlights-panel-border-radius: 0.8rem; */
|
|
251
|
-
/* --page-hero-highlights-panel-outline:
|
|
254
|
+
/* --page-hero-highlights-panel-outline: none; */
|
|
252
255
|
|
|
253
256
|
/* When using :highlight-title-baseline="true" */
|
|
254
257
|
/* --page-hero-highlights-title-height-baseline: 4rem; */ /* proportional value preferred */
|
|
@@ -268,10 +271,10 @@ row3: highlights bottom half
|
|
|
268
271
|
row4: page content (never underflows highlights)
|
|
269
272
|
```
|
|
270
273
|
|
|
271
|
-
`.page-hero-highlights-header-row` spans cols 1–3, rows 1–2 (edge-to-edge bg). `.page-hero-highlights-header-slot` is placed in row 1 only.
|
|
274
|
+
All of these sit in `.page-hero-highlights-grid`, the grid inside the root (the root is the size container). `.page-hero-highlights-header-row` spans cols 1–3, rows 1–2 (edge-to-edge bg). `.page-hero-highlights-header-slot` is placed in row 1 only.
|
|
272
275
|
`.page-hero-highlights-content-row` spans cols 1–3, rows 3–4 (bg fills behind highlights; `.page-hero-highlights-content-slot` is placed in row 4 only). The decorative border behind `.page-hero-highlights-content-slot` is rendered via `.page-hero-highlights-content-row::before` — there is no separate DOM element for it.
|
|
273
276
|
|
|
274
|
-
Grid columns are determined entirely by CSS — no `v-bind`. The `widthConstrained` and `contentAlign` props add CSS classes (`width-constrained`, `start`, `center`) which select the
|
|
277
|
+
Grid columns are determined entirely by CSS — no `v-bind`. The `widthConstrained` and `contentAlign` props add CSS classes (`width-constrained`, `content-align-start`, `content-align-center`) on the root, which select the `grid-template-columns` rule on `.page-hero-highlights-grid`.
|
|
275
278
|
|
|
276
279
|
## Layout pitfall: do not use `grid-template-rows: subgrid` inside `.page-hero-highlights-strip`
|
|
277
280
|
|
|
@@ -300,4 +303,5 @@ The `.page-hero-highlights-strip` element spans rows 2–3 of the parent grid (t
|
|
|
300
303
|
- Lives in `app/components/04.templates/page-hero-highlights/`.
|
|
301
304
|
- Storybook title: `"Templates/PageHeroHighlights"`.
|
|
302
305
|
- **`contentAlign` has no effect when `widthConstrained` is `false`** — both gutters hold their responsive default.
|
|
303
|
-
- **Gutter sizes are CSS tokens** — `--page-hero-highlights-gutter-mobile/tablet/desktop` are all overridable.
|
|
306
|
+
- **Gutter sizes are CSS tokens** — `--page-hero-highlights-gutter-mobile/tablet/desktop` are all overridable. They switch at 768px and 1024px of the component's own width (`@container page-hero-highlights`); the breakpoints themselves aren't tokens.
|
|
307
|
+
- **The root is an inline-size container**, so it can't size from its content: as a flex or grid item give it `inline-size: 100%` or a track. A grid on the root itself was moved to `.page-hero-highlights-grid` for this (an element can't query its own size).
|