srcdev-nuxt-components 9.6.1 → 9.6.3

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 (66) hide show
  1. package/.claude/rules/consumer/consumer-overrides.md +17 -0
  2. package/.claude/rules/consumer/css-comment-slash.md +13 -0
  3. package/.claude/rules/consumer/css-native-nesting.md +13 -0
  4. package/.claude/rules/consumer/css-no-deep.md +13 -0
  5. package/.claude/rules/consumer/css-rem-base.md +11 -0
  6. package/.claude/rules/consumer/css-surface-text-pairing.md +13 -0
  7. package/.claude/rules/consumer/css-webkit-support.md +15 -0
  8. package/.claude/rules/consumer/explicit-vue-imports.md +11 -0
  9. package/.claude/rules/consumer/nuxt-img-dimensions.md +12 -0
  10. package/.claude/rules/consumer/srcdev-skills-location.md +9 -0
  11. package/.claude/rules/consumer/story-args.md +12 -0
  12. package/.claude/rules/consumer/story-register-components.md +12 -0
  13. package/.claude/rules/consumer/vue-auto-imports.md +14 -0
  14. package/.claude/rules/consumer/vue-no-self-closing.md +11 -0
  15. package/.claude/rules/consumer/vue-prop-hyphenation.md +11 -0
  16. package/.claude/rules/consumer/vue-props-pattern.md +13 -0
  17. package/.claude/rules/consumer/vue-slots-no-computed.md +14 -0
  18. package/.claude/rules/consumer/vue-video-source.md +12 -0
  19. package/.claude/skills/component-compliance-checklist.md +37 -2
  20. package/.claude/skills/component-naming.md +1 -1
  21. package/.claude/skills/components/display-avatar.md +14 -3
  22. package/.claude/skills/components/display-chip.md +2 -1
  23. package/.claude/skills/components/google-reviews.md +2 -2
  24. package/.claude/skills/components/pricing-card.md +68 -84
  25. package/.claude/skills/components/profile-section.md +59 -21
  26. package/.claude/skills/components/social-icons-list.md +2 -0
  27. package/.claude/skills/index.md +13 -6
  28. package/.claude/skills/new-app-scaffold.md +4 -2
  29. package/.claude/skills/setup-postinstall.md +5 -4
  30. package/.claude/skills/storybook-add-story.md +38 -0
  31. package/.claude/skills/theming-component-token-pattern.md +94 -0
  32. package/.claude/skills/theming-partial-override.md +1 -1
  33. package/.vscode/srcdev-component-display-avatar.code-snippets +7 -0
  34. package/.vscode/srcdev-component-pricing-card.code-snippets +46 -84
  35. package/.vscode/srcdev-component-profile-section.code-snippets +27 -4
  36. package/README.md +5 -5
  37. package/app/components/01.atoms/animations/scroll-reveal-frame/stories/ScrollRevealFrame.stories.ts +2 -0
  38. package/app/components/01.atoms/animations/scroll-reveal-image/stories/ScrollRevealImage.stories.ts +2 -0
  39. package/app/components/01.atoms/animations/section-parallax/stories/SectionParallax.stories.ts +4 -0
  40. package/app/components/01.atoms/animations/view-timeline/stories/WipeAwayVertical.stories.ts +2 -0
  41. package/app/components/02.molecules/cookie-consent-banner/stories/CookieConsentBanner.stories.ts +2 -0
  42. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +26 -10
  43. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +75 -34
  44. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +84 -3
  45. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +85 -13
  46. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +3 -3
  47. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +7 -2
  48. package/app/components/02.molecules/input-copy/stories/InputCopy.stories.ts +0 -1
  49. package/app/components/02.molecules/pricing-card/CONSUMER-STYLING.md +96 -149
  50. package/app/components/02.molecules/pricing-card/PricingCard.vue +84 -44
  51. package/app/components/02.molecules/pricing-card/stories/PricingCard.stories.ts +241 -104
  52. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +149 -123
  53. package/app/components/02.molecules/profile-section/CONSUMER-STYLING.md +103 -13
  54. package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
  55. package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
  56. package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
  57. package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
  58. package/app/components/02.molecules/social-icons-list/CONSUMER-STYLING.md +6 -2
  59. package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
  60. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +4 -0
  61. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +4 -0
  62. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
  63. package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
  64. package/app/components/05.forms/toggle-switch/ToggleSwitchWithLabel.vue +1 -1
  65. package/app/composables/useAriaDescribedById.ts +1 -1
  66. package/package.json +2 -1
@@ -2,7 +2,11 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- `PricingCard` renders a SaaS-style pricing plan card with plan name, price, description, feature list, and call-to-action button. Supports "Most Popular" highlight state with optional badge. Features are highly customizable via CSS token overrides, and the CTA button can be customized via the `#cta` slot for custom variants and themes.
5
+ `PricingCard` (`app/components/02.molecules/pricing-card/`) renders a pricing plan card: plan name,
6
+ price, billing period, description, feature list and a call-to-action button. `is-highlighted`
7
+ adds a "Most Popular" badge plus a scaled, emphasised border; `ribbon-text` adds a diagonal corner
8
+ ribbon. The CTA can be replaced through the `#cta` slot. Styling tokens:
9
+ `app/components/02.molecules/pricing-card/CONSUMER-STYLING.md`.
6
10
 
7
11
  ---
8
12
 
@@ -10,34 +14,42 @@
10
14
 
11
15
  > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
12
16
 
13
- | Prop (template form) | Type | Default | Notes |
14
- | -------------------------- | ----------------------------- | ------------ | ------------------------------------------------------------------ |
15
- | `tag` | `"div" \| "section" \| "article"` | `"article"` | Root element tag for semantic HTML. |
16
- | `:plan-name` | `string` | (required) | Plan/tier name (e.g., "Basic", "Professional", "Enterprise"). |
17
- | `:price` | `number` | (required) | Price amount (e.g., `99`, `4.99`). Currency symbol added in template. |
18
- | `:billing-period` | `string` | `"one-time"` | Billing frequency text (e.g., "monthly", "yearly"). |
19
- | `:description` | `string` | (optional) | Short plan description. |
20
- | `:features` | `string[]` | `[]` | Array of feature strings. Rendered with checkmark icons. |
21
- | `:is-highlighted` | `boolean` | `false` | Highlight state; applies "Most Popular" badge, border, shadow, and scale. |
22
- | `:cta-text` | `string` | `"Get started"` | Button label text. |
23
- | `:cta-disabled` | `boolean` | `false` | Disables the CTA button. |
24
- | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the card root. |
17
+ | Prop (template form) | Type | Default | Notes |
18
+ | -------------------------- | -------------------------------------- | ---------------- | ----- |
19
+ | `tag` | `"div" \| "section" \| "article"` | `"article"` | Root element. Always labelled by the plan name heading (`aria-labelledby`). |
20
+ | `heading-tag` | `"h2" \| "h3" \| "h4" \| "h5" \| "h6"` | `"h3"` | Heading level for the plan name; pick to fit the page outline. |
21
+ | `plan-name` | `string` | (required) | Plan name. |
22
+ | `:price` | `number` | (required) | Whole numbers show as-is, fractional ones to two decimals (`9.5` → `9.50`). A non-finite value shows nothing. |
23
+ | `currency-symbol` | `string` | `"$"` | Literal prefix; no locale formatting. Pass `""` for none. |
24
+ | `billing-period` | `string` | `"one-time"` | Shown after the price; `""` hides it. |
25
+ | `description` | `string` | `undefined` | Short plan description. |
26
+ | `:features` | `string[]` | `[]` | Rendered as a checkmark list (checkmark hidden from screen readers). |
27
+ | `:is-highlighted` | `boolean` | `false` | Badge, highlight border/shadow, and `scale(1.05)`. |
28
+ | `badge-text` | `string` | `"Most Popular"` | Badge copy; pass a translated string for i18n. |
29
+ | `cta-text` | `string` | `"Get started"` | Default button label. |
30
+ | `:cta-disabled` | `boolean` | `false` | Sets the default button to `readonly`/`aria-disabled`. |
31
+ | `ribbon-text` | `string` | `undefined` | Corner ribbon copy; unset for no ribbon. Caps at two lines (`--pricing-card-ribbon-line-clamp`). |
32
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra classes on the root. |
33
+
34
+ All visitor-facing copy (`badge-text`, `cta-text`, `billing-period`) is a prop, so pass strings from
35
+ your own i18n solution.
25
36
 
26
37
  ---
27
38
 
28
39
  ## Events
29
40
 
30
- | Event | Payload | Notes |
31
- | ------ | ---------- | ---------------------------------------- |
32
- | `select` | `planName: string` | Emitted when the CTA button is clicked. |
41
+ | Event | Payload | Notes |
42
+ | -------- | ------------------ | ----- |
43
+ | `select` | `planName: string` | Emitted when the default CTA is clicked, or when a `#cta` slot calls `onSelect`. |
33
44
 
34
45
  ---
35
46
 
36
47
  ## Slots
37
48
 
38
- ### Default slot (features)
49
+ ### `#features`
39
50
 
40
- Override the default feature list rendering. Receives no slot scope.
51
+ Replaces the default `<li>` list. Your content goes inside the component's `<ul>`, so render `<li>`
52
+ elements. `--pricing-card-feature-line-clamp` and the checkmark only apply to the built-in list.
41
53
 
42
54
  ```vue
43
55
  <PricingCard plan-name="Pro" :price="99">
@@ -47,97 +59,57 @@ Override the default feature list rendering. Receives no slot scope.
47
59
  </PricingCard>
48
60
  ```
49
61
 
50
- ### #cta slot
62
+ ### `#cta`
51
63
 
52
- Customize the CTA button. Default renders `InputButton` with theme support.
64
+ Replaces the default `InputButton`. Slot scope:
53
65
 
54
- **Slot scope:**
55
- - `ctaText: string` — button label from `:cta-text` prop
56
- - `isDisabled: boolean` — button disabled state from `:cta-disabled` prop
57
- - `planName: string` — plan name for context
58
- - `onSelect: () => void` — callback to trigger `select` event
66
+ - `ctaText: string` — from `cta-text`
67
+ - `isDisabled: boolean` — from `cta-disabled`
68
+ - `planName: string`
69
+ - `onSelect: () => void` — emits `select`
59
70
 
60
71
  ```vue
61
72
  <PricingCard plan-name="Pro" :price="99" @select="handleSelect">
62
73
  <template #cta="{ ctaText, isDisabled, onSelect }">
63
- <InputButton
64
- :button-text="ctaText"
65
- variant="secondary"
66
- theme="custom"
67
- :readonly="isDisabled"
68
- @click="onSelect"
69
- />
74
+ <InputButton :button-text="ctaText" variant="secondary" :readonly="isDisabled" @click="onSelect" />
70
75
  </template>
71
76
  </PricingCard>
72
77
  ```
73
78
 
74
79
  ---
75
80
 
76
- ## CSS Token Customization
81
+ ## Styling
77
82
 
78
- All `--pricing-card-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
83
+ All `--pricing-card-*` tokens are listed in `CONSUMER-STYLING.md`. The common ones:
79
84
 
80
- **Common tokens:**
81
- - `--pricing-card-padding` — card internal spacing
82
- - `--pricing-card-border-radius` — card corners
83
- - `--pricing-card-cta-bg` — button background
84
- - `--pricing-card-highlight-border` — highlighted card border
85
- - `--pricing-card-name-color` — plan name text color
85
+ - `--pricing-card-padding`, `--pricing-card-border-radius`, `--pricing-card-background`
86
+ - `--pricing-card-highlight-border`, `--pricing-card-highlight-scale`
87
+ - `--pricing-card-name-line-clamp`, `--pricing-card-description-line-clamp`, `--pricing-card-feature-line-clamp` (default `none`)
88
+ - `--pricing-card-feature-icon-color` (checkmark colour)
89
+
90
+ The default CTA is styled with `InputButton`'s own tokens (`--input-button-primary-surface`, etc.),
91
+ set on the card or an ancestor. There are no `--pricing-card-cta-*` colour tokens: the old ones never
92
+ applied and were removed 2026-10-14. `--pricing-card-cta-align` still controls CTA alignment.
86
93
 
87
94
  ---
88
95
 
89
- ## Usage Examples
96
+ ## Usage examples
90
97
 
91
- ### Single card with default button
98
+ ### Single card
92
99
 
93
100
  ```vue
94
101
  <PricingCard
95
102
  plan-name="Basic"
96
103
  :price="29.99"
97
- billing-period="monthly"
104
+ currency-symbol="£"
105
+ billing-period="per month"
98
106
  description="Perfect for getting started"
99
107
  :features="['Feature 1', 'Feature 2', 'Feature 3']"
100
- cta-text="Get started"
101
- @select="handleSelect"
102
- />
103
- ```
104
-
105
- ### Highlighted "Most Popular" card
106
-
107
- ```vue
108
- <PricingCard
109
- plan-name="Professional"
110
- :price="79.99"
111
- billing-period="monthly"
112
- :is-highlighted="true"
113
- :features="['All Basic features', 'Priority support', 'Advanced analytics']"
114
108
  @select="handleSelect"
115
109
  />
116
110
  ```
117
111
 
118
- ### Card with custom button variant
119
-
120
- ```vue
121
- <PricingCard
122
- plan-name="Enterprise"
123
- :price="199.99"
124
- billing-period="monthly"
125
- description="Everything you need"
126
- :features="['Unlimited access', 'Dedicated support', 'Custom integrations']"
127
- @select="handleSelect"
128
- >
129
- <template #cta="{ ctaText, isDisabled, onSelect }">
130
- <InputButton
131
- :button-text="ctaText"
132
- variant="primary"
133
- :readonly="isDisabled"
134
- @click="onSelect"
135
- />
136
- </template>
137
- </PricingCard>
138
- ```
139
-
140
- ### Three-column grid with token customization
112
+ ### Comparison grid
141
113
 
142
114
  ```vue
143
115
  <div class="pricing-grid">
@@ -148,16 +120,28 @@ All `--pricing-card-*` tokens can be overridden at global, page, or instance sco
148
120
  :price="plan.price"
149
121
  :is-highlighted="plan.id === 'pro'"
150
122
  :features="plan.features"
123
+ heading-tag="h2"
151
124
  @select="handleSelect"
152
- style="--pricing-card-padding: 2.5rem; --pricing-card-cta-bg: var(--brand-primary)"
153
125
  />
154
126
  </div>
155
127
  ```
156
128
 
129
+ ```css
130
+ .pricing-grid {
131
+ display: grid;
132
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 26rem), 1fr));
133
+ gap: 2.4rem;
134
+ }
135
+ ```
136
+
157
137
  ---
158
138
 
159
139
  ## Notes
160
140
 
161
- - Feature checkmarks are always rendered with a teal checkmark (`✓`). Customize the color via `--teal-06` or component tokens.
162
- - The `#cta` slot is preferred over CSS-only button customization when you need variant or theme changes.
163
- - Highlighted cards apply a 1.05x scale transform — consider z-index when nesting in flex/grid layouts.
141
+ - The highlight scale is a `transform`, so it doesn't change layout size. In a stacked single
142
+ column set `--pricing-card-highlight-scale: 1`, or the highlighted card overhangs.
143
+ - The badge sits centred on the top border (`translate: 0 -50%`), so leave some space above the
144
+ card (grid gap or padding).
145
+ - The plan name gets `--pricing-card-ribbon-clearance` (`6rem`) of right padding while a ribbon is
146
+ shown, so long names don't run underneath it.
147
+ - Stories: `Molecules/Pricing Card`, including `Stress Test (Worst-Case Data)`.
@@ -1,24 +1,26 @@
1
1
  ---
2
2
  name: ProfileSection
3
- description: ProfileSection molecule — props, dynamic profile-info slots, eyebrowText/heroText/profileLinks slots, layout, accessibility
3
+ description: ProfileSection molecule — props, dynamic profile-info slots, eyebrowText/heroText/profileLinks slots, container-query layout, accessibility
4
4
  type: reference
5
5
  ---
6
6
 
7
7
  # ProfileSection
8
8
 
9
+ > **Changed 2026-10-14 (breaking):** New `headerPlacement` prop (`above`, `picture-bottom`, `picture-top`) can put the header over the bottom or top of the picture. `profileInfoCount` removed — only the `profile-info-N` slots you give are rendered (it used to pad with empty blocks when none were given). The two-column switch is a container query on the component's own width, not a viewport `@media` query, and both columns are `minmax(0, …)`. The `<header>` is only rendered when a header slot is given. Highlight colours go through new `--profile-section-*` tokens. An invalid `profilePicture.width`/`height` (0, negative, `NaN`) falls back to 828×1104.
10
+ >
9
11
  > **Classes renamed 2026-09-27:** `.picture` → `.profile-section-picture`, `.profile-picture` → `.profile-section-image`, `.profile-info` → `.profile-section-info`, `.profile-info-content` → `.profile-section-info-content`, `.profile-info-block` → `.profile-section-info-block`, `.profile-links` → `.profile-section-links`.
10
12
 
11
13
  ## Overview
12
14
 
13
- `ProfileSection` is a molecule that renders a practitioner/author profile: a header area (eyebrow + heading), a profile picture, and a flexible set of bio/info blocks alongside optional profile links. It is landmark-aware — when `tag` is `section`, `article`, or `aside`, the root element automatically gets `aria-labelledby` wired to the heading inside the `#heroText` slot (**you must bind `headingId` yourself, and a console warning fires if you forget** — see [component-aria-landmark.md](../component-aria-landmark.md)). `tag="main"` is never auto-labelled.
15
+ `ProfileSection` is a molecule that renders a practitioner/author profile: a header area (eyebrow + heading), a profile picture, and a flexible set of bio/info blocks alongside optional profile links. It is landmark-aware — when `tag` is `section` or `article`, the root element automatically gets `aria-labelledby` wired to the heading inside the `#heroText` slot (**you must bind `headingId` yourself, and a console warning fires if you forget** — see [component-aria-landmark.md](../component-aria-landmark.md)). `tag="main"` is never auto-labelled.
14
16
 
15
17
  ## Props
16
18
 
17
19
  | Prop | Type | Default | Description |
18
20
  |------|------|---------|-------------|
19
- | `profilePicture` | `ProfilePicture` (`{ src; alt; width?; height? }`, exported from the layer types) | — | **Required.** Rendered via `NuxtImg` with `width`/`height` defaulting to `828`/`1104` (3:4, a Vercel-allowed width). |
21
+ | `profilePicture` | `ProfilePicture` (`{ src; alt; width?; height? }`, exported from the layer types) | — | **Required.** Rendered via `NuxtImg`. `width`/`height` default to `828`/`1104` (3:4, a Vercel-allowed width), and a missing or invalid value (0, negative, `NaN`, `Infinity`) uses the default. |
20
22
  | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | HTML element rendered as the root. |
21
- | `profileInfoCount` | `number` | `3` | Number of `profile-info-N` slots to generate when none are explicitly provided. |
23
+ | `headerPlacement` | `"above" \| "picture-bottom" \| "picture-top"` | `"above"` | Where the `#eyebrowText`/`#heroText` header goes: above the grid, or over the bottom or top of the picture on a scrim. Template form: `header-placement`. |
22
24
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
23
25
 
24
26
  ## Slots
@@ -28,17 +30,19 @@ type: reference
28
30
  | `#eyebrowText` | Optional eyebrow label above the heading. Use `EyebrowText`. |
29
31
  | `#heroText` | Heading slot. Receives `headingId` as a slot prop — bind it to the heading's `id` for accessible `aria-labelledby`. |
30
32
  | `#profile-info-1` … `#profile-info-N` | Bio / info blocks. Provide as many numbered slots as needed. They are rendered in ascending numeric order. |
31
- | `#profileLinks` | Links / actions shown at the bottom-right of the info column (e.g. social icons, booking CTA). |
33
+ | `#profileLinks` | Links / actions shown at the bottom-right of the info column (e.g. social icons, booking CTA). Wraps onto more lines when there are many. |
34
+
35
+ The `<header class="profile-section-header">` is only rendered when `#eyebrowText` or `#heroText` is given. It renders above the grid, or inside the picture when `header-placement` is `picture-bottom` or `picture-top`.
32
36
 
33
37
  ## Basic usage
34
38
 
35
39
  ```vue
36
40
  <ProfileSection
37
41
  tag="section"
38
- :profile-picture="{ src: '/images/profile/mel.jpg', alt: 'Mel Stafford, reflexologist' }"
42
+ :profile-picture="{ src: '/images/profile/alex.jpg', alt: 'Alex Morgan, hair stylist' }"
39
43
  >
40
44
  <template #eyebrowText>
41
- <EyebrowText font-size="large" text="About Mel" />
45
+ <EyebrowText font-size="large" text-content="About Alex" />
42
46
  </template>
43
47
 
44
48
  <template #heroText="{ headingId }">
@@ -47,8 +51,8 @@ type: reference
47
51
  :id="headingId"
48
52
  font-size="title"
49
53
  :text-content="[
50
- { text: 'Mel Stafford', styleClass: 'normal' },
51
- { text: 'Reflexologist', styleClass: 'accent' },
54
+ { text: 'Alex Morgan', styleClass: 'normal' },
55
+ { text: 'Hair stylist', styleClass: 'accent' },
52
56
  ]"
53
57
  />
54
58
  </template>
@@ -58,7 +62,7 @@ type: reference
58
62
  </template>
59
63
 
60
64
  <template #profile-info-2>
61
- <p>Mel is based in Lichfield…</p>
65
+ <p class="location">Based in <span class="highlight">Northtown</span>…</p>
62
66
  </template>
63
67
 
64
68
  <template #profileLinks>
@@ -69,11 +73,12 @@ type: reference
69
73
 
70
74
  ## Dynamic profile-info slots
71
75
 
72
- The `profile-info-N` slots are discovered at runtime by filtering `useSlots()` for keys matching `/^profile-info-\d+$/`. The slots are rendered in ascending numeric order.
76
+ The `profile-info-N` slots are discovered at runtime by filtering `useSlots()` for keys matching `/^profile-info-\d+$/`, and rendered in ascending numeric order.
73
77
 
74
78
  - If you provide `#profile-info-1`, `#profile-info-2`, `#profile-info-3` — all three render in order.
75
- - If you provide no `profile-info-*` slots, the component renders `profileInfoCount` empty blocks (they used to contain visible placeholder text, removed 2026-09-27). Slot discovery runs every render, so slots added after mount appear.
76
- - Gaps are supported — you can provide `#profile-info-1` and `#profile-info-3` without `#profile-info-2`; they sort correctly.
79
+ - If you provide none, no info blocks render.
80
+ - Gaps are supported — you can provide `#profile-info-1` and `#profile-info-3` without `#profile-info-2`; `#profile-info-12` sorts after `#profile-info-2`.
81
+ - Discovery runs every render, so slots added or removed after mount (a `v-if` on the slot template) appear and disappear.
77
82
 
78
83
  ## heroText slot prop
79
84
 
@@ -89,10 +94,41 @@ If you use a different heading component, bind `headingId` to whatever prop rend
89
94
 
90
95
  ## Layout
91
96
 
92
- - **Mobile**: single column — picture stacked above info.
93
- - **768px+**: two columns — `--profile-section-picture-width` (`384px`) picture column, `1fr` info column, `--profile-section-gap-wide` (`4rem`) gap (`--profile-section-gap`, `2rem`, below 768px).
94
- - Picture frame (`.profile-section-picture`): `--profile-section-picture-aspect-ratio` (`3 / 4`), `--profile-section-picture-border-radius` (`8px`), `overflow: hidden`. Full token list: `CONSUMER-STYLING.md` in the component folder. The `NuxtImg` fills the frame with `object-fit: cover`.
95
- - Profile links: `align-items: end; justify-content: flex-end` — right-aligned to the bottom of the info column.
97
+ - The root is a size container named `profile-section`. Layout follows **the component's own width**, not the viewport.
98
+ - **Under 768px wide**: single column — picture stacked above info, `--profile-section-gap` (`2rem`).
99
+ - **768px wide or more**: two columns — `minmax(0, var(--profile-section-picture-width))` (`38.4rem`) picture column, `minmax(0, 1fr)` info column, `--profile-section-gap-wide` (`4rem`) gap.
100
+ - Being an inline-size container, the root can't size itself from its content: give it a width in a flex row or other shrink-to-fit context.
101
+ - The root sets `overflow-wrap: anywhere`, so unbroken text and URLs in any slot wrap rather than widening the grid.
102
+ - Picture frame (`.profile-section-picture`): `--profile-section-picture-aspect-ratio` (`3 / 4`), `--profile-section-picture-border-radius` (`0.8rem`), `overflow: hidden`. The `NuxtImg` fills the frame with `object-fit: cover`. Full token list: `CONSUMER-STYLING.md` in the component folder.
103
+ - Profile links: `flex-wrap: wrap; align-items: end; justify-content: flex-end` — right-aligned to the bottom of the info column.
104
+ - To change the picture column width, set `--profile-section-picture-width`; don't override `grid-template-columns` in a viewport `@media` query, which no longer lines up with the component's own switch.
105
+
106
+ ## Header in picture
107
+
108
+ With `header-placement="picture-bottom"` or `"picture-top"`, the `<header>` renders inside `.profile-section-picture`, before the image in the DOM (so it is read first), and sits at the bottom or top of the frame on a scrim that fades away from that edge. It follows ServicesCard's `titles-within-image-wrapper`:
109
+
110
+ - Scrim and text come from `--profile-section-picture-header-*` tokens: scrim colour (`#000`) and opacity (`75%`), a solid stop (`0%`, how far the scrim stays full strength before fading) and a target colour (`transparent`, what it fades into, set it to roughly the photo's colour where the fade ends so it blends instead of leaving a grey band), text colour (`contrast-color()` of the scrim colour where supported, else `#fff`), eyebrow colour, padding (`2.4rem`), gap (`0.4rem`). Full list in `CONSUMER-STYLING.md`.
111
+ - The frame drops `overflow: hidden` in this mode, so its aspect ratio is a minimum: a long header grows the frame rather than being clipped. The image is absolutely positioned and still fills the frame.
112
+ - `aria-labelledby` still points at the heading.
113
+ - Drop spacing utilities like `mb-20` from the heading in this mode; the header's padding handles spacing.
114
+
115
+ ```vue
116
+ <ProfileSection tag="section" header-placement="picture-bottom" :profile-picture="picture">
117
+ <template #eyebrowText>
118
+ <EyebrowText tag="p" font-size="medium" text-content="About Alex" />
119
+ </template>
120
+ <template #heroText="{ headingId }">
121
+ <HeroText :id="headingId" tag="h2" font-size="heading" :text-content="[{ text: 'Alex Morgan', styleClass: 'normal' }]" />
122
+ </template>
123
+ ...
124
+ </ProfileSection>
125
+ ```
126
+
127
+ Two stories show themes built only from tokens: `Header In Picture (Dark, Bottom)`, ServicesCard-style, and `Header In Picture (Light, Top)`. A light scrim needs a dark `--profile-section-picture-header-text-colour`; `contrast-color()` picks one where supported, but set it for older browsers. `--profile-section-info-text-colour` sets the info column's text colour for a themed background.
128
+
129
+ ## Content hooks
130
+
131
+ A `.highlight` inside an element with class `location` gets `--profile-section-highlight-colour` (default `var(--colour-text-accent)`); inside `services` it gets `--profile-section-link-colour` / `--profile-section-link-colour-hover` (defaults `var(--colour-link-default)` / `var(--colour-link-hover)`). Both use `--profile-section-highlight-font-weight` (`600`). The `location`/`services` class can sit on the `<p>` itself or a wrapper.
96
132
 
97
133
  ## CSS notes
98
134
 
@@ -106,8 +142,6 @@ If you use a different heading component, bind `headingId` to whatever prop rend
106
142
  }
107
143
  ```
108
144
 
109
- - Accent-coloured text within `.location` or `.services` info blocks can use the `.highlight` class for `var(--colour-text-accent)` or `var(--colour-link-default)` colouring respectively.
110
-
111
145
  ## Consumer styling scaffold
112
146
 
113
147
  ```vue
@@ -121,6 +155,8 @@ If you use a different heading component, bind `headingId` to whatever prop rend
121
155
  <style>
122
156
  .profile-section {
123
157
  &.my-profile {
158
+ --profile-section-picture-width: 32rem;
159
+
124
160
  .profile-section-header {
125
161
  margin-block-end: 3.2rem;
126
162
  }
@@ -131,6 +167,8 @@ If you use a different heading component, bind `headingId` to whatever prop rend
131
167
 
132
168
  ## Notes
133
169
 
134
- - `NuxtImg` is used for the profile picture — provide a real `src` path. A placeholder (grey box) is shown if the image 404s.
135
- - The component does not expose `imgWidth` / `imgHeight` props for the picture. If IPX optimisation is critical, use a fixed-dimension image and rely on the `384px` column cap as the natural size constraint.
170
+ - `NuxtImg` is used for the profile picture — provide a real `src` path. A broken image shows its alt text inside the 3:4 frame.
171
+ - Pass `width`/`height` in `profilePicture` when your image isn't 3:4 at 828px wide, so IPX/Vercel request the right size.
172
+ - The `With Social Icons` story shows a `SocialIconsList` in `#profileLinks`; size it with `--social-icons-list-icon-size` and `--social-icons-list-gap` on your own wrapper.
173
+ - The `StressTest` story covers hostile content (broken image, unbroken text, RTL, out-of-order slots, many links) at every canvas width.
136
174
  - Component is auto-imported in Nuxt — no import needed.
@@ -57,6 +57,8 @@ import type { ISocialIcon } from "srcdev-nuxt-components";
57
57
  | YouTube | `logos:youtube-icon` | `https://www.youtube.com/@` |
58
58
  | TikTok | `logos:tiktok-icon` | `https://www.tiktok.com/@` |
59
59
 
60
+ `logos:instagram-icon`, `logos:x` and other dark brand icons vanish on a dark background, and their colours can't be changed. On a dark background use a mono icon such as `mdi:instagram` (any set that draws in `currentColor`) and colour it with `--social-icons-list-icon-colour` (default `currentColor`). Installed mono Instagram icons: `mdi:instagram`, `bi:instagram`, `lucide:instagram`, `akar-icons:instagram-fill`, `radix-icons:instagram-logo`.
61
+
60
62
  ---
61
63
 
62
64
  ## Usage example
@@ -7,10 +7,16 @@ Step-by-step guides for repeatable development tasks in this project.
7
7
  Copy skills into your project with:
8
8
 
9
9
  ```bash
10
- cp -r node_modules/srcdev-nuxt-components/.claude/skills .claude/skills/srcdev-nuxt-components
10
+ rm -rf .claude/skills/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components
11
11
  ```
12
12
 
13
- Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run without overwriting your own skills.
13
+ Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run: it replaces only that folder, so skills dropped or renamed upstream are removed and your own skills are never touched.
14
+
15
+ The layer also ships a curated set of Claude Code rules in `.claude/rules/consumer/`. `npm run setup:claude` (see `setup-postinstall.md`) copies skills and rules together; to copy the rules by hand:
16
+
17
+ ```bash
18
+ rm -rf .claude/rules/srcdev-nuxt-components && mkdir -p .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components
19
+ ```
14
20
 
15
21
  ## Structure
16
22
 
@@ -32,7 +38,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
32
38
  ├── theming-override-default.md — replace the entire default theme with a custom palette (set --theme-hue/--theme-chroma)
33
39
  ├── theming-partial-override.md — override a specific colour token category (palette, buttons, inputs) without a full theme replacement
34
40
  ├── theming-form-geometry-tokens.md — non-colour form/button/input tokens (padding, border-radius, gaps): full inventory, why partial override doesn't need duplicating the source files
35
- ├── theming-component-token-pattern.md — standard shape for overridable component colours: public token + inline fallback, no bare var()/no private-only indirection; the three tiers (theme slots / cross-component families / component-scoped); line-clamp tokens for overflowing text; rollout status
41
+ ├── theming-component-token-pattern.md — standard shape for overridable component colours: public token + inline fallback, no bare var()/no private-only indirection; the three tiers (theme slots / cross-component families / component-scoped); line-clamp tokens for overflowing text; image-overlay scrim tokens; rollout status
36
42
  ├── theming-status-tokens.md — global --status-* colours (success/warning/danger/info/neutral: solid + surface + text), which components read them, how to override once for the whole library
37
43
  ├── theming-dark-mode.md — add a dark scheme in a consumer app (layer is light-only): enable plumbing, full dark token set without light-dark(), light-dark() alternative, light-only pinning
38
44
  ├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
@@ -97,7 +103,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
97
103
  ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
98
104
  ├── content-docs.md — ContentDocs docs-page shell: prop-driven docsNav/docsPageNav (not slots), DocsNavItem icons, container-width breakpoint behaviour, shared/per-side CSS token API
99
105
  ├── glass-panel.md — GlassPanel: frosted-glass container, light-only token defaults in-component (light-dark() block removed from _default.css, no built-in dark glass), highlight behind content, section/article labelled via headingId slot prop, CSS token API
100
- ├── pricing-card.md — PricingCard: SaaS-style plan card with highlight, feature list, #cta slot for button customization, CSS token API
106
+ ├── pricing-card.md — PricingCard: plan card with highlight badge, corner ribbon, feature list, #cta slot, heading-tag, line-clamp tokens
101
107
  ├── price-list.md — PriceList: service/menu price columns (dl rows), PriceListData type, headingTag/fromLabel props, CSS token API
102
108
  ├── opening-hours.md — OpeningHours: per-day data auto-grouped into ranges, split sessions, 24h/appointment, exceptions, Intl formatting, today highlight, JSON-LD, CSS token API
103
109
  ├── input-copy.md — InputCopy: readonly copy-to-clipboard input, visual feedback, Clipboard API, accessibility, CSS token API
@@ -108,12 +114,13 @@ Each skill is a single markdown file named `<area>-<task>.md`.
108
114
  ├── wipe-away-vertical.md — WipeAwayVertical: scroll-driven vertical wipe-away effect, pure-CSS grid overlay (no JS), indexed dynamic slots, sticky-centering-via-calc gotcha, CSS tokens
109
115
  ├── marquee-scroller.md — MarqueeScroller: infinite logo/badge scroller, per-item dynamic slots (marqueeData id), hover/focus/keyboard pause, reduced-motion, CSS tokens
110
116
  ├── rotating-carousel-image.md — RotatingCarouselImage: 3D rotating image carousel, scroll-parallax tilt, focus/keyboard/hover pause, reduced-motion, CSS tokens
111
- ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
117
+ ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names (dark ones vanish on dark backgrounds; mono alternatives), CSS tokens incl. --social-icons-list-icon-colour
112
118
  ├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
113
119
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
114
120
  ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
115
121
  ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-gap/min-col-size-{small,default,large} tokens, is-responsive, semantic tag + aria
116
- ├── display-avatar.md — DisplayAvatar (02.molecules): circular avatar with image/initials fallback (alt announced), size scale tokens, --display-avatar-* colours, chip badge, icon slot
122
+ ├── display-avatar.md — DisplayAvatar (02.molecules): circular avatar with image/initials fallback (alt announced), broken-image fallback, data-size scale tokens, image-size, --display-avatar-* colours, chip badge, icon slot
123
+ ├── profile-section.md — ProfileSection (02.molecules): picture + numbered profile-info-N slots, eyebrow/hero header (above, or over the picture top/bottom on a scrim via header-placement), profileLinks, container-query two-column layout, .location/.services highlight tokens
117
124
  ├── display-theme-switch.md — DisplayThemeSwitch: system/light/dark picker wrapping TripleToggleSwitch, wired to useSettingsStore, labels/icons props, small sizing variant
118
125
  ├── card-core.md — CardCore: generic card container, dynamic named slots as rows, 4 variants, blurred backdrop layer, full CSS token API
119
126
  ├── action-menu.md — ActionMenu + ActionMenuItem: ellipsis trigger + anchored popover menu, indexed item-{n} slots, link/button items, full CSS token API
@@ -44,7 +44,7 @@ to inform the CLAUDE.md and CSS tokens.
44
44
  "dev": "nuxt dev",
45
45
  "generate": "nuxt generate",
46
46
  "preview": "nuxt preview",
47
- "setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
47
+ "setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
48
48
  "postinstall": "NUXT_STANDALONE=true nuxt prepare && npm run setup:claude",
49
49
  "lint": "eslint .",
50
50
  "lint:fix": "eslint . --fix"
@@ -71,7 +71,9 @@ to inform the CLAUDE.md and CSS tokens.
71
71
 
72
72
  > **`setup:claude` script**: `mkdir -p` is required — the destination folder won't exist on a
73
73
  > fresh clone. The trailing `/.` on the source path copies contents into the destination rather
74
- > than nesting a `skills` subfolder inside it.
74
+ > than nesting a `skills` subfolder inside it. The leading `rm -rf` clears only the two
75
+ > `srcdev-nuxt-components` folders, so skills and rules dropped or renamed upstream don't linger
76
+ > (a stale rule stays loaded); the app's own `.claude/skills/` and `.claude/rules/` are untouched.
75
77
 
76
78
  ### 3. Create `nuxt.config.ts`
77
79
 
@@ -5,7 +5,7 @@
5
5
  Prevent common "forgot to run after install" problems in a consuming app:
6
6
 
7
7
  1. `nuxt prepare` — generates Nuxt type declarations. Skipping it causes TypeScript errors after install or package updates.
8
- 2. `npm run setup:claude` — copies the latest `srcdev-nuxt-components` skills into `.claude/skills/srcdev-nuxt-components/`. Skipping it leaves Claude working from stale skill docs after a package update.
8
+ 2. `npm run setup:claude` — copies the latest `srcdev-nuxt-components` skills into `.claude/skills/srcdev-nuxt-components/` and its consumer rules into `.claude/rules/srcdev-nuxt-components/`. Skipping it leaves Claude working from stale skill docs and rules after a package update.
9
9
 
10
10
  Additionally, `srcdev-nuxt-components` automatically copies VSCode snippet files (`.code-snippets`) to your `.vscode/` folder during its own postinstall, so components' code snippets are available immediately.
11
11
 
@@ -17,7 +17,7 @@ A consumer app's `postinstall` script runs both automatically after every `npm i
17
17
 
18
18
  ```json
19
19
  "scripts": {
20
- "setup:claude": "mkdir -p .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components",
20
+ "setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
21
21
  "postinstall": "node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude"
22
22
  }
23
23
  ```
@@ -26,7 +26,7 @@ This postinstall hook does three things in order:
26
26
 
27
27
  1. Copies VSCode snippets from the layer to your `.vscode/` folder
28
28
  2. Runs `nuxt prepare` to generate type declarations
29
- 3. Runs `setup:claude` to copy skills into `.claude/skills/srcdev-nuxt-components/`
29
+ 3. Runs `setup:claude` to copy skills into `.claude/skills/srcdev-nuxt-components/` and rules into `.claude/rules/srcdev-nuxt-components/`
30
30
 
31
31
  ### 2. Check whether your app needs an env flag for nuxt prepare
32
32
 
@@ -49,6 +49,7 @@ From this point on, `npm install` and `npm ci` trigger both steps automatically.
49
49
  ## Notes
50
50
 
51
51
  - **Snippet copy must run in consumer app**: npm doesn't invoke postinstall hooks for dependencies. Each consumer app must explicitly call `scripts/copy-snippets.mjs` in its own postinstall to copy VSCode snippets to `.vscode/`. The layer's postinstall is used when the layer runs standalone, but consumers are responsible for their own `.vscode/` folder.
52
- - Skills land in `.claude/skills/srcdev-nuxt-components/` — safe to re-run without overwriting your own project's skills.
52
+ - Skills land in `.claude/skills/srcdev-nuxt-components/` and rules in `.claude/rules/srcdev-nuxt-components/` — safe to re-run. The script deletes and recreates only those two folders, so anything dropped or renamed upstream is removed (a stale rule would otherwise stay loaded forever); your own project's skills and rules are never touched.
53
+ - Only the layer's `.claude/rules/consumer/` set ships. The rest of its rules govern the library's own source (naming, ledger, demo pages) and would be wrong in an app.
53
54
  - `postinstall` also fires on `npm ci`, so CI environments get the skills and snippets too if they have a `.claude/` directory in the project.
54
55
  - If you do not want `postinstall` running in CI, guard it: `"postinstall": "[ \"$CI\" = \"true\" ] || (node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude)"`.
@@ -268,6 +268,8 @@ export const Default: Story = {
268
268
  Key points:
269
269
 
270
270
  - `computed` is **not** auto-imported in `.ts` story files — import it explicitly from `"vue"`.
271
+ - Components aren't auto-registered in a story's `template` string either. Every component used there, including ones you put in the component's slots (`EyebrowText`, `HeroText`, `SocialIconsList`, ...), must be imported and listed in `components: {}`. Otherwise Vue renders it as an empty unknown element (`<eyebrowtext>`) with no error. ProfileSection's header and social-icons stories rendered blank this way until 2026-10-14.
272
+ - Props in a story's `template` string aren't type-checked either. An invalid value renders silently with no warning: ProfileSection's stories passed `font-size="large"` to `HeroText`, which only has `display`/`title`/`heading`/`subheading`/`label`, so the heading fell back to unstyled text. Check every literal prop value against the component's `interface Props`.
271
273
  - Extra args must be stripped before `v-bind` — spreading unknown keys onto a component makes them unknown HTML attributes.
272
274
  - CSS custom properties set via `:style` on the component root are picked up by `var()` in the component's scoped CSS.
273
275
 
@@ -285,6 +287,29 @@ prop and try to pass it. Make it unmistakable:
285
287
 
286
288
  `PriceList.stories.ts` and `GoogleReviews.stories.ts` (line-clamp controls) follow this.
287
289
 
290
+ ## Docs page
291
+
292
+ `.storybook/preview.ts` sets `tags: ["autodocs"]` globally (2026-10-14), so every story file gets a
293
+ generated Docs page and the meta doesn't need the tag. Before that, only two files opted in, and
294
+ the earlier nightly `@nuxtjs/storybook` didn't render Docs pages at all, so a lot of existing
295
+ `docs.description` text had never been visible. Check the page whenever you add or change a story
296
+ (compliance checklist item 6c):
297
+
298
+ - `parameters.docs.description.component` in the meta: what the component is for and its main
299
+ props, slots and events. `docs.description.story` on a story: what that story shows.
300
+ - The Docs page renders every story inline in one document. Overlays (`position: fixed`), dialogs,
301
+ toasts, fullscreen canvases and scroll-driven effects break or overlap there. Render them in
302
+ their own iframe instead:
303
+
304
+ ```ts
305
+ parameters: {
306
+ docs: { story: { inline: false, height: "480px" } },
307
+ },
308
+ ```
309
+
310
+ - Opt a file out with `tags: ["!autodocs"]` only when it isn't a component reference (tooling, or
311
+ a file with its own attached `.mdx` page, like `ColourRamps.stories.ts` with `SetupGuide.mdx`).
312
+
288
313
  ## Link to the stories of components that use this one
289
314
 
290
315
  When the component you're working on is a building block of other components (it's rendered
@@ -360,6 +385,19 @@ story: "See the [CookieConsentBanner](?path=/story/molecules-cookieconsentbanner
360
385
  controls that make sense (hide irrelevant ones with `Story.argTypes = { x: { table: { disable: true } } }`),
361
386
  or, for a fixed showcase (comparisons, all-themes grids, composition examples), add
362
387
  `controls: { disable: true }` to that story's `parameters`.
388
+ - **Sidebar order** comes from the `storySort` function in `.storybook/preview.ts`: top-level
389
+ groups in tier order, everything below them alphabetical, entries within one component in file
390
+ order with Docs first. Storybook doesn't run that function from the compiled file. It extracts
391
+ the function's source and `eval`s it as plain JavaScript, so any TypeScript inside it
392
+ (annotations, `as` casts) or a reference to something outside its body fails the whole preview
393
+ build with `SyntaxError: Unexpected token ':'` (found 2026-10-15). It stays annotation-free
394
+ under a `// @ts-expect-error`. Check an edit without starting Storybook:
395
+
396
+ ```bash
397
+ node --input-type=module -e 'import { getStorySortParameter } from "storybook/internal/csf-tools"; import fs from "node:fs"; console.log(typeof getStorySortParameter(fs.readFileSync(".storybook/preview.ts", "utf8")));'
398
+ ```
399
+
400
+ `function` means it parses; a syntax error is the same one Storybook would hit.
363
401
 
364
402
  ## Scroll/animation-driven effects need surrounding chrome, not just the bare component
365
403