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.
Files changed (67) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +14 -0
  2. package/.claude/skills/component-compliance-checklist.md +6 -0
  3. package/.claude/skills/component-naming.md +2 -2
  4. package/.claude/skills/components/input-checkbox-radio.md +19 -3
  5. package/.claude/skills/components/input-checkbox.md +14 -3
  6. package/.claude/skills/components/input-description.md +1 -1
  7. package/.claude/skills/components/input-error.md +1 -1
  8. package/.claude/skills/components/input-label.md +2 -2
  9. package/.claude/skills/components/input-number.md +44 -13
  10. package/.claude/skills/components/input-radio.md +15 -1
  11. package/.claude/skills/components/{input-range-core.md → input-range.md} +53 -18
  12. package/.claude/skills/components/input-textarea-core.md +4 -4
  13. package/.claude/skills/components/page-hero-highlights.md +22 -18
  14. package/.claude/skills/index.md +6 -6
  15. package/.claude/skills/testing-add-unit-test.md +17 -0
  16. package/.vscode/srcdev-component-input-checkbox-radio.code-snippets +0 -1
  17. package/.vscode/srcdev-component-input-range.code-snippets +38 -18
  18. package/.vscode/srcdev-component-page-hero-highlights.code-snippets +2 -1
  19. package/app/components/04.templates/page-hero-highlights/CONSUMER-STYLING.md +32 -12
  20. package/app/components/04.templates/page-hero-highlights/PageHeroHighlights.vue +56 -47
  21. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlights.stories.ts +158 -307
  22. package/app/components/04.templates/page-hero-highlights/stories/PageHeroHighlightsHeader.stories.ts +10 -0
  23. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlights.spec.ts +49 -27
  24. package/app/components/04.templates/page-hero-highlights/tests/PageHeroHighlightsHeader.spec.ts +42 -7
  25. package/app/components/04.templates/page-hero-highlights/tests/__snapshots__/PageHeroHighlights.spec.ts.snap +12 -10
  26. package/app/components/05.forms/input-checkbox/CONSUMER-STYLING.md +10 -0
  27. package/app/components/05.forms/input-checkbox/MultipleCheckboxes.vue +18 -4
  28. package/app/components/05.forms/input-checkbox/SingleCheckbox.vue +14 -2
  29. package/app/components/05.forms/input-checkbox/stories/MultipleCheckboxes.stories.ts +146 -37
  30. package/app/components/05.forms/input-checkbox/stories/SingleCheckbox.stories.ts +107 -30
  31. package/app/components/05.forms/input-checkbox/tests/MultipleCheckboxes.spec.ts +38 -4
  32. package/app/components/05.forms/input-checkbox/tests/SingleCheckbox.spec.ts +5 -0
  33. package/app/components/05.forms/input-checkbox-radio/CONSUMER-STYLING.md +10 -3
  34. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadio.vue +1 -0
  35. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioButton.vue +11 -10
  36. package/app/components/05.forms/input-checkbox-radio/InputCheckboxRadioField.vue +3 -0
  37. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadio.stories.ts +44 -17
  38. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioButton.stories.ts +164 -58
  39. package/app/components/05.forms/input-checkbox-radio/stories/InputCheckboxRadioField.stories.ts +94 -21
  40. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadio.spec.ts +14 -3
  41. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioButton.spec.ts +38 -3
  42. package/app/components/05.forms/input-checkbox-radio/tests/InputCheckboxRadioField.spec.ts +14 -3
  43. package/app/components/05.forms/input-description/CONSUMER-STYLING.md +2 -2
  44. package/app/components/05.forms/input-label/CONSUMER-STYLING.md +2 -2
  45. package/app/components/05.forms/input-number/CONSUMER-STYLING.md +42 -2
  46. package/app/components/05.forms/input-number/InputNumber.vue +37 -25
  47. package/app/components/05.forms/input-number/InputNumberField.vue +29 -16
  48. package/app/components/05.forms/input-number/stories/InputNumber.stories.ts +76 -25
  49. package/app/components/05.forms/input-number/stories/InputNumberField.stories.ts +116 -50
  50. package/app/components/05.forms/input-number/tests/InputNumber.spec.ts +49 -13
  51. package/app/components/05.forms/input-number/tests/InputNumberField.spec.ts +59 -5
  52. package/app/components/05.forms/input-radio/CONSUMER-STYLING.md +11 -0
  53. package/app/components/05.forms/input-radio/MultipleRadiobuttons.vue +14 -3
  54. package/app/components/05.forms/input-radio/stories/MultipleRadiobuttons.stories.ts +138 -38
  55. package/app/components/05.forms/input-radio/tests/MultipleRadioButtons.spec.ts +50 -3
  56. package/app/components/05.forms/input-range/CONSUMER-STYLING.md +68 -26
  57. package/app/components/05.forms/input-range/InputRange.vue +247 -0
  58. package/app/components/05.forms/input-range/{InputRangeDefault.vue → InputRangeField.vue} +41 -18
  59. package/app/components/05.forms/input-range/stories/InputRange.stories.ts +187 -0
  60. package/app/components/05.forms/input-range/stories/InputRangeField.stories.ts +196 -0
  61. package/app/components/05.forms/input-range/tests/{InputRangeCore.spec.ts → InputRange.spec.ts} +39 -15
  62. package/app/components/05.forms/input-range/tests/{InputRangeDefault.spec.ts → InputRangeField.spec.ts} +71 -7
  63. package/app/components/05.forms/patterns/stories/MigratedFieldsForm.stories.ts +5 -5
  64. package/package.json +1 -1
  65. package/app/components/05.forms/input-range/InputRangeCore.vue +0 -169
  66. package/app/components/05.forms/input-range/stories/InputRangeCore.stories.ts +0 -126
  67. package/app/components/05.forms/input-range/stories/InputRangeDefault.stories.ts +0 -108
@@ -88,7 +88,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
88
88
  ├── link-text.md — LinkText props, slots, usage patterns, styling
89
89
  ├── header-block.md — HeaderBlock: tagLevel/classLevel semantic-vs-visual heading decoupling, page-heading-N utility classes
90
90
  ├── text-block.md — TextBlock: vertical-rhythm text wrapper, --text-block-padding-block-start/-end tokens, heading-id slot prop for section/article aria-labelledby
91
- ├── page-hero-highlights.md — PageHeroHighlights template: hero + highlights strip grid, CSS custom property theming
91
+ ├── page-hero-highlights.md — PageHeroHighlights template: hero + straddling highlights strip grid (root is the `page-hero-highlights` container, grid in `.page-hero-highlights-grid`), widthConstrained caps the column, equal-width highlights wrap at --page-hero-highlights-item-min-width, light theme-token defaults, PageHeroHighlightsHeader companion
92
92
  ├── slider-gallery.md — SliderGallery: full-screen image carousel, IGalleryData, pause-on-hover/reduced-motion auto-advance, prefixed classes, CSS tokens
93
93
  ├── services-card.md — ServicesCard props (incl. eyebrowConfig/heroConfig), actions slot, CSS tokens, page boilerplate
94
94
  ├── google-reviews.md — GoogleReviews + GoogleReviewCard: Google Places reviews in a scrolling row; env vars, server route, useGoogleReviews, props, slots, tokens, Google terms
@@ -147,8 +147,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
147
147
  ├── pop-over.md — PopOver: generic anchor-positioned disclosure panel, consumer-supplied trigger/content slots, placement prop, CSS token API
148
148
  ├── input-text-core.md — InputTextCore: native text/date/number input primitive, min/max pass-through (date-picker range), CSS token API; Variants section covers InputTextWithLabel/InputPasswordWithLabel/InputTextAsNumberWithLabel
149
149
  ├── input-button.md — InputButton (renamed from InputButtonCore 2026-09-28): button/NuxtLink/a, variants, icon slots, readonly (aria-disabled, activation blocked), tokens
150
- ├── input-range-core.md — InputRangeCore: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeDefault
151
- ├── input-number.md — InputNumber: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberField
150
+ ├── input-range.md — InputRange: native range-slider primitive, markers/datalist slots, dead --theme-form-range-accent-color token fixed, CSS token API; Variants section covers InputRangeField; renamed from InputRangeCore/InputRangeDefault 2026-10-16, clamped stepping, useId datalist id, data-* hooks
151
+ ├── input-number.md — InputNumber: native number-input primitive, left/right step-button slots, missing base class/placeholder/mismatched slot-selector bugs fixed, CSS token API; Variants section covers InputNumberField; clamped, rounded stepping, data-* state hooks, min/step-aware inputmode
152
152
  ├── input-otp.md — InputOtp: one-time-code digit boxes (paste/autofill spreading, Backspace/arrow/Home/End nav, Enter submits, leading zeros kept, complete event, hidden input); Variants section covers InputOtpField
153
153
  ├── form-field.md — FormField: layout wrapper for one form control (width cap, gutter, field spacing), data-width/data-has-gutter/data-invalid hooks, dead .underline rule removed, CSS token API
154
154
  ├── form-fieldset.md — FormFieldset: shared fieldset+legend for checkbox/radio groups, groupRole prop (hardcoded radiogroup bug fixed), aria-required only on radiogroup, dead description slot removed, legend token API
@@ -159,9 +159,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
159
159
  ├── pending-effect.md — PendingEffect: animated dashed border InputButton draws while is-pending (has-pending-effect), tokens renamed to --pending-effect-* and read at point of use, animation moved off the host button, aria-hidden
160
160
  ├── input-textarea-core.md — InputTextareaCore: native textarea primitive, left/right decorative slots, undefined-token/dead-code/label-leak bugs fixed, CSS token API; Variants section covers InputTextareaWithLabel
161
161
  ├── input-select-core.md — InputSelectCore: native select primitive, data-driven options with icon decorator, dead required/styleClassPassthrough/isDirty-isActive functionality fixed, appearance:base-select browser-support note, CSS token API; Variants section covers InputSelectWithLabel
162
- ├── input-checkbox-radio.md — InputCheckboxRadio (bare control), InputCheckboxRadioField (labelled), InputCheckboxRadioButton (button/pill option): renamed from Core/WithLabel, state classes → data-* hooks, type-switch v-model fix, no aria-checked, icon font-size sizing, button spacing tokens
163
- ├── input-checkbox.md — MultipleCheckboxes/SingleCheckbox: fieldset-wrapped checkbox group (data-driven, button or labelled) and single checkbox, per-box native-required/styleClassPassthrough/data-testid bugs fixed, spacing token API
164
- ├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API
162
+ ├── input-checkbox-radio.md — InputCheckboxRadio (bare control), InputCheckboxRadioField (labelled), InputCheckboxRadioButton (button/pill option): renamed from Core/WithLabel, state classes → data-* hooks, type-switch v-model fix, no aria-checked, icon font-size sizing, button spacing tokens, long labels wrap (inline nowrap removed), optional useId button id
163
+ ├── input-checkbox.md — MultipleCheckboxes/SingleCheckbox: fieldset-wrapped checkbox group (data-driven, button or labelled) and single checkbox, per-box native-required/styleClassPassthrough/data-testid bugs fixed, spacing token API, wraps hostile content (fieldset min-inline-size, useId option ids)
164
+ ├── input-radio.md — MultipleRadiobuttons: fieldset radiogroup (data-driven, button or labelled), shared-name grouping bug fixed, dead label/placeholder/equalCols props removed, data-options-layout hook, spacing token API, useId button ids, wraps hostile content
165
165
  ├── toggle-switch-core.md — ToggleSwitchCore: pill/square checkbox-backed toggle primitive, dead round-prop/data-theme-error functionality and story theme-options bugs fixed, CSS token API; Variants section covers ToggleSwitchWithLabel (descriptionText/descriptionHtml via InputDescription, `description` slot removed 2026-09-29)/ToggleSwitchWithLabelInline
166
166
  ├── triple-toggle-switch.md — TripleToggleSwitch (renamed from TripleToggleSwitchCore): three-option icon radio pill with sliding marker, radiogroup + ariaLabel, system/light/dark marker-gradient tokens, reduced-motion, CSS token API
167
167
  ├── entry-animation.md — EntryAnimation: scroll-driven entry animation wrapper (slide-in/zoom-reveal/exit-blur utility classes), skipAnimation for above-the-fold loop items, reduced-motion handled at the CSS layer
@@ -321,6 +321,23 @@ it("passes myProp to ChildComponent", async () => {
321
321
 
322
322
  Import the child component directly in the test file — it is not auto-imported there.
323
323
 
324
+ ## Testing `useId()` uniqueness across instances
325
+
326
+ Each `mountSuspended` call is its own Nuxt app, so `useId()` restarts its counter and two separately
327
+ mounted instances get the **same** ids. That's a test artefact, not a bug. To prove two instances
328
+ don't collide, render both inside one host component (`MultipleCheckboxes.spec.ts`):
329
+
330
+ ```ts
331
+ const Host = defineComponent({
332
+ components: { ComponentUnderTest },
333
+ setup: () => ({ props: initialPropsData }),
334
+ template: `<div><ComponentUnderTest v-bind="props" /><ComponentUnderTest v-bind="props" /></div>`,
335
+ });
336
+ const host = await mountSuspended(Host);
337
+ const ids = host.findAll("input").map((input) => input.attributes("id"));
338
+ expect(new Set(ids).size).toBe(ids.length);
339
+ ```
340
+
324
341
  ## Mocking browser APIs
325
342
 
326
343
  ### Global constructors (ResizeObserver, IntersectionObserver, etc.)
@@ -30,7 +30,6 @@
30
30
  "body": [
31
31
  "<InputCheckboxRadioButton",
32
32
  " v-for=\"option in ${1:options}\"",
33
- " :id=\"`${2:name}-\\${option.value}`\"",
34
33
  " :key=\"option.value\"",
35
34
  " v-model=\"${3:state.value}\"",
36
35
  " type=\"${4|checkbox,radio|}\"",
@@ -1,9 +1,9 @@
1
1
  {
2
- "SRCDEV InputRangeCore Basic": {
3
- "description": "Basic InputRangeCore native range input (no label/error wrapper)",
2
+ "SRCDEV InputRange Basic": {
3
+ "description": "Basic InputRange native range input (no label/error wrapper)",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<InputRangeCore",
6
+ "<InputRange",
7
7
  " id=\"$1fieldId\"",
8
8
  " name=\"$2fieldName\"",
9
9
  " v-model=\"$3fieldValue\"",
@@ -13,11 +13,11 @@
13
13
  "/>"
14
14
  ]
15
15
  },
16
- "SRCDEV InputRangeCore With Markers": {
17
- "description": "InputRangeCore with custom tick-mark markers overlaid on the track",
16
+ "SRCDEV InputRange With Markers": {
17
+ "description": "InputRange with custom tick-mark markers overlaid on the track",
18
18
  "scope": "vue,html",
19
19
  "body": [
20
- "<InputRangeCore",
20
+ "<InputRange",
21
21
  " id=\"$1fieldId\"",
22
22
  " name=\"$2fieldName\"",
23
23
  " v-model=\"$3fieldValue\"",
@@ -27,19 +27,39 @@
27
27
  ">",
28
28
  " <template #markers>",
29
29
  " <div class=\"input-range-markers\">",
30
- " <div class=\"marker\"><span class=\"marker-icon\">$7\\ud83d\\ude00</span></div>",
31
- " <div class=\"marker\"><span class=\"marker-icon\">$8\\ud83d\\ude10</span></div>",
32
- " <div class=\"marker\"><span class=\"marker-icon\">$9\\ud83d\\ude1e</span></div>",
30
+ " <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$7\\ud83d\\ude00</span></div>",
31
+ " <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$8\\ud83d\\ude10</span></div>",
32
+ " <div class=\"input-range-marker\"><span class=\"input-range-marker-icon\">$9\\ud83d\\ude1e</span></div>",
33
33
  " </div>",
34
34
  " </template>",
35
- "</InputRangeCore>"
35
+ "</InputRange>"
36
36
  ]
37
37
  },
38
- "SRCDEV InputRangeDefault Basic": {
39
- "description": "InputRangeDefault: labelled range slider with error/description support",
38
+ "SRCDEV InputRange With Datalist": {
39
+ "description": "InputRange with a datalist; the slot passes the generated datalistId",
40
40
  "scope": "vue,html",
41
41
  "body": [
42
- "<InputRangeDefault",
42
+ "<InputRange",
43
+ " id=\"$1fieldId\"",
44
+ " name=\"$2fieldName\"",
45
+ " v-model=\"$3fieldValue\"",
46
+ " :min=\"${4:0}\"",
47
+ " :max=\"${5:100}\"",
48
+ ">",
49
+ " <template #datalist=\"{ datalistId }\">",
50
+ " <datalist :id=\"datalistId\" class=\"input-range-datalist\">",
51
+ " <option value=\"${4:0}\" label=\"${6:Low}\"></option>",
52
+ " <option value=\"${5:100}\" label=\"${7:High}\"></option>",
53
+ " </datalist>",
54
+ " </template>",
55
+ "</InputRange>"
56
+ ]
57
+ },
58
+ "SRCDEV InputRangeField Basic": {
59
+ "description": "InputRangeField: labelled range slider with error/description support",
60
+ "scope": "vue,html",
61
+ "body": [
62
+ "<InputRangeField",
43
63
  " v-model=\"$1fieldValue\"",
44
64
  " name=\"$2fieldName\"",
45
65
  " label=\"$3Field label\"",
@@ -51,11 +71,11 @@
51
71
  "/>"
52
72
  ]
53
73
  },
54
- "SRCDEV InputRangeDefault With Step Buttons": {
55
- "description": "InputRangeDefault with step-down/step-up buttons wired into the left/right slots",
74
+ "SRCDEV InputRangeField With Step Buttons": {
75
+ "description": "InputRangeField with step-down/step-up buttons wired into the left/right slots",
56
76
  "scope": "vue,html",
57
77
  "body": [
58
- "<InputRangeDefault",
78
+ "<InputRangeField",
59
79
  " v-model=\"$1fieldValue\"",
60
80
  " name=\"$2fieldName\"",
61
81
  " label=\"$3Field label\"",
@@ -67,11 +87,11 @@
67
87
  ">",
68
88
  " <template #left>\\u2212</template>",
69
89
  " <template #right>+</template>",
70
- "</InputRangeDefault>"
90
+ "</InputRangeField>"
71
91
  ]
72
92
  },
73
93
  "SRCDEV InputRange CSS Override": {
74
- "description": "CSS override scaffold for InputRangeCore tokens",
94
+ "description": "CSS override scaffold for InputRange tokens",
75
95
  "scope": "css",
76
96
  "body": [
77
97
  ".$1my-page {",
@@ -3,7 +3,7 @@
3
3
  "description": "PageHeroHighlights: header zone, straddling highlights strip, content panel",
4
4
  "scope": "vue,html",
5
5
  "body": [
6
- "<PageHeroHighlights tag=\"section\" content-align=\"${1|center,start|}\">",
6
+ "<PageHeroHighlights tag=\"section\" :width-constrained=\"true\" content-align=\"${1|center,start|}\">",
7
7
  " <template #header=\"{ headingId }\">",
8
8
  " <PageHeroHighlightsHeader>",
9
9
  " <template #start><h1 :id=\"headingId\">$2</h1></template>",
@@ -31,6 +31,7 @@
31
31
  " --page-hero-highlights-content-background: $3;",
32
32
  " --page-hero-highlights-item-background: $4;",
33
33
  " --page-hero-highlights-max-width: $5;",
34
+ " --page-hero-highlights-item-min-width: $6;",
34
35
  "}"
35
36
  ]
36
37
  }
@@ -6,12 +6,12 @@
6
6
 
7
7
  | Token | Default | Controls |
8
8
  |---|---|---|
9
- | `--page-hero-highlights-max-width` | `1064px` | Central column cap (unless `width-constrained`) |
9
+ | `--page-hero-highlights-max-width` | `1064px` | Central column cap with `width-constrained` |
10
10
  | `--page-hero-highlights-gutter-mobile` | `16px` | Side gutter |
11
- | `--page-hero-highlights-gutter-tablet` | `40px` | Side gutter at 768px+ container width |
12
- | `--page-hero-highlights-gutter-desktop` | `32px` | Side gutter at 1024px+ |
13
- | `--page-hero-highlights-header-background` | `darkblue` | Header zone background |
14
- | `--page-hero-highlights-content-background` | `var(--slate-01)` | Content zone background |
11
+ | `--page-hero-highlights-gutter-tablet` | `40px` | Side gutter at 768px+ of the component's width |
12
+ | `--page-hero-highlights-gutter-desktop` | `32px` | Side gutter at 1024px+ of the component's width |
13
+ | `--page-hero-highlights-header-background` | `var(--theme-surface-subtle)` | Header zone background |
14
+ | `--page-hero-highlights-content-background` | `transparent` | Content zone background |
15
15
  | `--page-hero-highlights-content-start-gap` | `1.2rem` | Space above the content panel |
16
16
  | `--page-hero-highlights-content-end-gap` | `1.2rem` | Space below the content zone |
17
17
  | `--page-hero-highlights-content-margin-block-start` | `2.4rem` | Space above the content slot |
@@ -23,24 +23,25 @@
23
23
  |---|---|---|
24
24
  | `--page-hero-highlights-strip-gap` | `1rem` | Gap between highlight items |
25
25
  | `--page-hero-highlights-strip-inset` | `1.2rem` | Strip inline inset (with the content panel) |
26
+ | `--page-hero-highlights-item-min-width` | `12rem` | Narrowest an item gets with `highlights-equal-widths` before the strip wraps to a new row |
26
27
  | `--page-hero-highlights-title-height` | `1fr` | Title row height |
27
28
  | `--page-hero-highlights-title-height-baseline` | `4rem` | Title row height with `highlight-title-baseline` |
28
29
  | `--page-hero-highlights-item-padding` | `1.2rem` | Item padding |
29
30
  | `--page-hero-highlights-item-padding-block-start` | the item padding | Item top padding |
30
31
  | `--page-hero-highlights-item-padding-block-start-baseline` | `0` | Item top padding with `highlight-title-baseline` |
31
32
  | `--page-hero-highlights-item-rows-gap` | `1.2rem` | Gap between item title and body |
32
- | `--page-hero-highlights-item-background` | `white` | Item background |
33
- | `--page-hero-highlights-item-border` | `1px solid black` | Item border |
33
+ | `--page-hero-highlights-item-background` | `var(--page-bg, Canvas)` | Item background |
34
+ | `--page-hero-highlights-item-border` | `1px solid var(--theme-border)` | Item border |
34
35
  | `--page-hero-highlights-item-border-radius` | `8px` | Item rounding |
35
- | `--page-hero-highlights-item-colour` | `black` | Item text colour |
36
+ | `--page-hero-highlights-item-colour` | `var(--theme-text)` | Item text colour |
36
37
 
37
38
  ### Content panel (`content-panel`, on by default)
38
39
 
39
40
  | Token | Default | Controls |
40
41
  |---|---|---|
41
- | `--page-hero-highlights-panel-background` | `var(--slate-00)` | Panel background |
42
- | `--page-hero-highlights-panel-border` | `1px solid var(--slate-06)` | Panel border |
43
- | `--page-hero-highlights-panel-outline` | `1px solid var(--slate-02)` | Panel outline |
42
+ | `--page-hero-highlights-panel-background` | `var(--theme-surface-subtle)` | Panel background |
43
+ | `--page-hero-highlights-panel-border` | `1px solid var(--theme-border)` | Panel border |
44
+ | `--page-hero-highlights-panel-outline` | `none` | Panel outline |
44
45
  | `--page-hero-highlights-panel-border-radius` | `0.8rem` | Panel rounding |
45
46
 
46
47
  ### PageHeroHighlightsHeader
@@ -56,6 +57,20 @@
56
57
  Private (not public API): `--_max-width`, `--_gutter`, `--_strip-inset`, `--_title-height`,
57
58
  `--_item-padding`, `--_item-padding-block-start`, `--_header-slot-grid-row`.
58
59
 
60
+ > Changed 2026-10-16:
61
+ >
62
+ > - `width-constrained` was inverted: `true` gave a full-width column and `false` capped it at
63
+ > `--page-hero-highlights-max-width`, the opposite of every doc. It now matches the docs (`true` caps it,
64
+ > `content-align` applies). Flip any `width-constrained` you set to get the old look.
65
+ > - The tablet and desktop gutters never applied: their `@container` queries had no container to
66
+ > match. The root is now a named `page-hero-highlights` inline-size container and the rows sit in a
67
+ > new `.page-hero-highlights-grid` wrapper, so the gutters step at the component's own width.
68
+ > - Colour defaults are light, from theme tokens (were `darkblue`, `white`, `black` and `--slate-*`):
69
+ > set the header, item and panel tokens to keep a dark header.
70
+ > - `highlights-equal-widths` now wraps onto new rows below `--page-hero-highlights-item-min-width`
71
+ > instead of squeezing every item into one row, and grid tracks are `minmax(0, …)` so long
72
+ > unbroken text can't widen the layout. Highlight items get `overflow-wrap: anywhere`.
73
+ >
59
74
  > Changed 2026-09-28: every token used to be declared on the root with its value, under generic
60
75
  > names (`--max-width`, `--header-row-background-colour`, `--highlight-*`, `--content-row-*`,
61
76
  > `--content-slot-*`, `--phh-*`), so a value set on an ancestor never landed and the names could
@@ -74,7 +89,7 @@ Private (not public API): `--_max-width`, `--_gutter`, `--_strip-inset`, `--_tit
74
89
  | `.highlight-title-baseline` | root | `highlight-title-baseline` |
75
90
  | `.equal-widths` / `.flexible-widths`, `.justify-*` | `.page-hero-highlights-strip` | `highlights-equal-widths`, `highlights-justify` |
76
91
 
77
- Inner classes: `.page-hero-highlights-header-row`, `.page-hero-highlights-header-slot`,
92
+ Inner classes: `.page-hero-highlights-grid` (the grid itself, inside the root), `.page-hero-highlights-header-row`, `.page-hero-highlights-header-slot`,
78
93
  `.page-hero-highlights-strip`, `.page-hero-highlights-content-row` (its `::before` is the content
79
94
  panel), `.page-hero-highlights-content-slot`; header component `.page-hero-highlights-header-start`,
80
95
  `.page-hero-highlights-header-end`.
@@ -96,6 +111,11 @@ Set the tokens above on an element you own (a page or section class, or a class
96
111
  `<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
97
112
  `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
98
113
 
114
+ **Caveat:** the root is an inline-size container (`container: page-hero-highlights / inline-size`), so it
115
+ can't size itself from its content: give it a width from its context (a block in normal flow is fine; as a
116
+ flex or grid item give it `inline-size: 100%` or a track). The highlight item padding tokens are
117
+ re-declared by `highlight-title-baseline`, so with that prop on, set the `-baseline` variants.
118
+
99
119
  ---
100
120
 
101
121
  ## Class passthrough
@@ -5,17 +5,19 @@
5
5
  :class="[elementClasses, componentClasses]"
6
6
  :aria-labelledby="ariaLabelledby"
7
7
  >
8
- <div class="page-hero-highlights-header-row">
9
- <div class="page-hero-highlights-header-slot">
10
- <slot name="header" :heading-id="headingId"></slot>
8
+ <div class="page-hero-highlights-grid">
9
+ <div class="page-hero-highlights-header-row">
10
+ <div class="page-hero-highlights-header-slot">
11
+ <slot name="header" :heading-id="headingId"></slot>
12
+ </div>
11
13
  </div>
12
- </div>
13
- <div class="page-hero-highlights-strip" :class="highlightClasses">
14
- <slot name="highlights"></slot>
15
- </div>
16
- <div class="page-hero-highlights-content-row">
17
- <div class="page-hero-highlights-content-slot">
18
- <slot name="content"></slot>
14
+ <div class="page-hero-highlights-strip" :class="highlightClasses">
15
+ <slot name="highlights"></slot>
16
+ </div>
17
+ <div class="page-hero-highlights-content-row">
18
+ <div class="page-hero-highlights-content-slot">
19
+ <slot name="content"></slot>
20
+ </div>
19
21
  </div>
20
22
  </div>
21
23
  </component>
@@ -79,13 +81,7 @@ watch(
79
81
  --_item-padding-block-start: var(--page-hero-highlights-item-padding-block-start, var(--_item-padding));
80
82
  --_header-slot-grid-row: 1;
81
83
 
82
- @container (width >= 768px) {
83
- --_gutter: var(--page-hero-highlights-gutter-tablet, 40px);
84
- }
85
-
86
- @container (width >= 1024px) {
87
- --_gutter: var(--page-hero-highlights-gutter-desktop, 32px);
88
- }
84
+ container: page-hero-highlights / inline-size;
89
85
 
90
86
  &.highlight-title-baseline {
91
87
  --_title-height: var(--page-hero-highlights-title-height-baseline, 4rem);
@@ -93,38 +89,44 @@ watch(
93
89
  --_header-slot-grid-row: 1 / span 2;
94
90
  }
95
91
 
96
- display: grid;
97
- grid-template-rows: auto var(--_title-height) 1fr auto;
98
- gap: 0;
99
-
100
- &.width-constrained {
101
- grid-template-columns: var(--_gutter) 1fr var(--_gutter);
102
- }
92
+ .page-hero-highlights-grid {
93
+ display: grid;
94
+ grid-template-rows: auto var(--_title-height) 1fr auto;
95
+ grid-template-columns: var(--_gutter) minmax(0, 1fr) var(--_gutter);
96
+ gap: 0;
103
97
 
104
- &:not(.width-constrained) {
105
- &.content-align-start {
106
- grid-template-columns: var(--_gutter) minmax(0, var(--_max-width)) minmax(var(--_gutter), 1fr);
98
+ @container page-hero-highlights (width >= 768px) {
99
+ --_gutter: var(--page-hero-highlights-gutter-tablet, 40px);
107
100
  }
108
101
 
109
- &.content-align-center {
110
- grid-template-columns:
111
- max(var(--_gutter), (100% - var(--_max-width)) / 2)
112
- 1fr
113
- max(var(--_gutter), (100% - var(--_max-width)) / 2);
102
+ @container page-hero-highlights (width >= 1024px) {
103
+ --_gutter: var(--page-hero-highlights-gutter-desktop, 32px);
114
104
  }
115
105
  }
116
106
 
107
+ &.width-constrained.content-align-start .page-hero-highlights-grid {
108
+ grid-template-columns: var(--_gutter) minmax(0, var(--_max-width)) minmax(var(--_gutter), 1fr);
109
+ }
110
+
111
+ &.width-constrained.content-align-center .page-hero-highlights-grid {
112
+ grid-template-columns:
113
+ max(var(--_gutter), (100% - var(--_max-width)) / 2)
114
+ minmax(0, 1fr)
115
+ max(var(--_gutter), (100% - var(--_max-width)) / 2);
116
+ }
117
+
117
118
  .page-hero-highlights-header-row {
118
119
  grid-column: 1 / -1;
119
120
  grid-row: 1 / 3;
120
121
  display: grid;
121
122
  grid-template-columns: subgrid;
122
123
  grid-template-rows: subgrid;
123
- background-color: var(--page-hero-highlights-header-background, darkblue);
124
+ background-color: var(--page-hero-highlights-header-background, var(--theme-surface-subtle));
124
125
 
125
126
  .page-hero-highlights-header-slot {
126
127
  grid-column: 2;
127
128
  grid-row: var(--_header-slot-grid-row);
129
+ min-inline-size: 0;
128
130
  container-type: inline-size;
129
131
  }
130
132
  }
@@ -134,19 +136,27 @@ watch(
134
136
  grid-row: 2 / 4;
135
137
  position: relative;
136
138
  z-index: 1;
139
+ min-inline-size: 0;
137
140
  gap: var(--page-hero-highlights-strip-gap, 1rem);
138
141
  margin-inline-start: 0;
139
142
 
140
143
  &.equal-widths {
141
144
  display: grid;
142
- grid-auto-columns: 1fr;
143
- grid-auto-flow: column;
145
+ grid-template-columns: repeat(
146
+ auto-fit,
147
+ minmax(min(100%, var(--page-hero-highlights-item-min-width, 12rem)), 1fr)
148
+ );
144
149
  }
145
150
 
146
151
  &.flexible-widths {
147
152
  display: flex;
148
153
  flex-wrap: wrap;
149
154
  justify-content: center;
155
+
156
+ & > * {
157
+ min-inline-size: 0;
158
+ max-inline-size: 100%;
159
+ }
150
160
  }
151
161
 
152
162
  &.justify-start {
@@ -173,18 +183,19 @@ watch(
173
183
  display: grid;
174
184
  grid-template-rows: subgrid;
175
185
  grid-auto-flow: row;
186
+ overflow-wrap: anywhere;
176
187
  padding-block: var(--_item-padding-block-start) var(--_item-padding);
177
188
  padding-inline: var(--_item-padding);
178
- background-color: var(--page-hero-highlights-item-background, white);
179
- border: var(--page-hero-highlights-item-border, 1px solid black);
189
+ background-color: var(--page-hero-highlights-item-background, var(--page-bg, Canvas));
190
+ border: var(--page-hero-highlights-item-border, 1px solid var(--theme-border));
180
191
  border-radius: var(--page-hero-highlights-item-border-radius, 8px);
181
- color: var(--page-hero-highlights-item-colour, black);
192
+ color: var(--page-hero-highlights-item-colour, var(--theme-text));
182
193
 
183
194
  .page-hero-highlights-item-title {
184
195
  display: grid;
185
196
  grid-row: 2;
186
197
  align-items: end;
187
- height: var(--_title-height);
198
+ block-size: var(--_title-height);
188
199
  }
189
200
 
190
201
  .page-hero-highlights-item-body {
@@ -202,12 +213,13 @@ watch(
202
213
  grid-template-rows: subgrid;
203
214
  position: relative;
204
215
  isolation: isolate;
205
- background-color: var(--page-hero-highlights-content-background, var(--slate-01));
216
+ background-color: var(--page-hero-highlights-content-background, transparent);
206
217
  padding-block-end: var(--page-hero-highlights-content-end-gap, 1.2rem);
207
218
 
208
219
  .page-hero-highlights-content-slot {
209
220
  grid-column: 2;
210
221
  grid-row: 2;
222
+ min-inline-size: 0;
211
223
  margin-block: var(--page-hero-highlights-content-margin-block-start, 2.4rem)
212
224
  var(--page-hero-highlights-content-margin, var(--_strip-inset));
213
225
  margin-inline: 0;
@@ -222,15 +234,12 @@ watch(
222
234
  .page-hero-highlights-content-row {
223
235
  &::before {
224
236
  content: "";
225
- display: grid;
226
- grid-template-columns: subgrid;
227
- grid-template-rows: subgrid;
228
237
  grid-column: 2;
229
238
  grid-row: 1 / span 2;
230
- margin-top: var(--page-hero-highlights-content-start-gap, 1.2rem);
231
- background-color: var(--page-hero-highlights-panel-background, var(--slate-00));
232
- border: var(--page-hero-highlights-panel-border, 1px solid var(--slate-06));
233
- outline: var(--page-hero-highlights-panel-outline, 1px solid var(--slate-02));
239
+ margin-block-start: var(--page-hero-highlights-content-start-gap, 1.2rem);
240
+ background-color: var(--page-hero-highlights-panel-background, var(--theme-surface-subtle));
241
+ border: var(--page-hero-highlights-panel-border, 1px solid var(--theme-border));
242
+ outline: var(--page-hero-highlights-panel-outline, none);
234
243
  border-radius: var(--page-hero-highlights-panel-border-radius, 0.8rem);
235
244
  }
236
245