srcdev-nuxt-components 9.2.5 → 9.2.7

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 (35) hide show
  1. package/.claude/settings.json +35 -1
  2. package/.claude/skills/components/content-docs.md +165 -0
  3. package/.claude/skills/components/expanding-panel.md +55 -0
  4. package/.claude/skills/index.md +4 -2
  5. package/.claude/skills/qa-panel.md +5 -6
  6. package/.claude/skills/theming-typography-tokens.md +109 -0
  7. package/.vscode/srcdev-component-content-docs.code-snippets +129 -0
  8. package/.vscode/srcdev-component-expanding-panel.code-snippets +123 -0
  9. package/app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue +410 -0
  10. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts +53 -0
  11. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/default-chromium-darwin.png +0 -0
  12. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-desktop-chromium-darwin.png +0 -0
  13. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-mobile-chromium-darwin.png +0 -0
  14. package/app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts-snapshots/state-tablet-chromium-darwin.png +0 -0
  15. package/app/components/01.atoms/content-wrappers/docs-pages/stories/ContentDocs.stories.ts +172 -0
  16. package/app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts +218 -0
  17. package/app/components/01.atoms/page-row/PageRow.vue +2 -0
  18. package/app/components/02.molecules/expandable/expanding-panel/CONSUMER-STYLING.md +103 -0
  19. package/app/components/02.molecules/expandable/expanding-panel/ExpandingPanel.vue +97 -56
  20. package/app/components/02.molecules/expandable/expanding-panel/stories/ExpandingPanel.stories.ts +50 -2
  21. package/app/components/02.molecules/expandable/expanding-panel/tests/ExpandingPanel.spec.ts +79 -2
  22. package/app/components/02.molecules/expandable/expanding-panel/tests/__snapshots__/ExpandingPanel.spec.ts.snap +25 -4
  23. package/app/components/02.molecules/pricing-card/tests/PricingCard.spec.ts +2 -1
  24. package/app/components/05.forms/input-button/InputButtonCore.vue +12 -1
  25. package/app/components/05.forms/input-button/tests/InputButtonCore.spec.ts +28 -0
  26. package/app/components/layout-grids/LayoutGridA.vue +59 -59
  27. package/app/composables/tests/useContainerBreakpoints.spec.ts +91 -0
  28. package/app/composables/useContainerBreakpoints.ts +71 -0
  29. package/app/layouts/default.vue +1 -0
  30. package/app/pages/ui/expanding-panel.vue +266 -129
  31. package/app/pages/ui/layout-content-docs.vue +72 -0
  32. package/app/pages/ui/layout-grid-a.vue +5 -58
  33. package/app/types/components/content-docs.d.ts +5 -0
  34. package/app/types/components/index.ts +1 -0
  35. package/package.json +1 -1
@@ -59,7 +59,41 @@
59
59
  "Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-services-section.code-snippets','utf8'\\)\\); console.log\\('valid json'\\)\")",
60
60
  "WebFetch(domain:luxury-locs-by-natasha-nuxt3.vercel.app)",
61
61
  "Bash(cd /Users/simoncornforth/websites/nuxt-components *)",
62
- "Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-banner-video.code-snippets','utf8'\\)\\); console.log\\('valid json'\\)\")"
62
+ "Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-banner-video.code-snippets','utf8'\\)\\); console.log\\('valid json'\\)\")",
63
+ "Bash(python3 -c \"import json; json.load\\(open\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-expanding-panel.code-snippets'\\)\\)\")",
64
+ "Bash(node -e \"JSON.parse\\(require\\('fs'\\).readFileSync\\('/Users/simoncornforth/websites/nuxt-components/.vscode/srcdev-component-expanding-panel.code-snippets','utf8'\\)\\); console.log\\('OK'\\)\")",
65
+ "Bash(npx eslint *)",
66
+ "Bash(git stash *)",
67
+ "Bash(rm /Users/simoncornforth/websites/nuxt-components/app/components/02.molecules/expandable/expanding-panel/tests/repro.spec.ts)",
68
+ "Bash(npx playwright *)",
69
+ "Bash(break)",
70
+ "Bash(cp /tmp/pw-test.mjs /Users/simoncornforth/websites/nuxt-components/pw-test.mjs)",
71
+ "Bash(node pw-test.mjs)",
72
+ "Bash(ls ~/Library/Caches/ms-playwright 2>/dev/null)",
73
+ "Read(//Users/simoncornforth/Library/Caches/ms-playwright/**)",
74
+ "Bash(lsof -nP -iTCP:3000 -sTCP:LISTEN)",
75
+ "Bash(ps -p 3241 -o pid,etime,command)",
76
+ "Bash(pkill -f \"nuxt dev\" -u simoncornforth)",
77
+ "Bash(lsof -nP -iTCP:3000,3001 -sTCP:LISTEN)",
78
+ "Bash(curl -s http://localhost:3000/ui/layout-grid-a -o /tmp/a.html -w \"%{http_code}\\\\n\")",
79
+ "Bash(curl -s http://localhost:3000/ui/layout-grid-b -o /tmp/b.html -w \"%{http_code}\\\\n\")",
80
+ "Bash(grep -o \"desktopCanvas[^,{]*{[^}]*}\" /tmp/a.html)",
81
+ "Bash(grep -o \"desktopCanvas[^,{]*{[^}]*}\" /tmp/b.html)",
82
+ "Bash(curl -s \"http://localhost:3000/_nuxt/components/canvas-switcher/CanvasSwitcher.vue?vue&type=style&index=0&lang.css\")",
83
+ "Bash(ps -p 9159 -o pid,command)",
84
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\\\\n\" http://localhost:3000/ui/layout-content-docs)",
85
+ "Bash(node /private/tmp/claude-501/-Users-simoncornforth-websites-nuxt-components/3e6d42b1-c0ff-4b71-a082-656f9e15e24c/scratchpad/debug-breakpoints.mjs)",
86
+ "Bash(sed -i '' 's/from \"playwright\"/from \"@playwright\\\\/test\"/' /Users/simoncornforth/websites/nuxt-components/debug-breakpoints.mjs)",
87
+ "Bash(node debug-breakpoints.mjs)",
88
+ "Bash(grep -v \"ERR_PACKAGE_PATH_NOT_EXPORTED\\\\|Resolve plugin\\\\|at exportsNot\\\\|at packageExports\\\\|at resolveExports\\\\|at Module\\\\|at wrapResolve\\\\|at resolveForCJS\\\\|at resolve \\\\|at CompilerOptions\\\\|at Object.createParsed\\\\|code: 'ERR'\\\\|^}$\")",
89
+ "Bash(lsof -i :6006 -sTCP:LISTEN -t)",
90
+ "Bash(curl -s \"http://127.0.0.1:6006/index.json\")",
91
+ "Bash(curl -s \"http://127.0.0.1:6006/iframe.html?id=atoms-content-wrappers-contentdocs--default&viewMode=story\" -o /dev/null -w \"%{http_code}\\\\n\")",
92
+ "Bash(SRCDEV_STANDALONE=true npx playwright test app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts --project=chromium --update-snapshots)",
93
+ "Bash(SRCDEV_STANDALONE=true npx playwright test app/components/01.atoms/content-wrappers/docs-pages/playwright/content-docs.playwright.ts --project=chromium)",
94
+ "Bash(git rm *)",
95
+ "Bash(awk '{print $1}')",
96
+ "Bash(git reset *)"
63
97
  ],
64
98
  "additionalDirectories": [
65
99
  "/Users/simoncornforth/websites/instepreflexology",
@@ -0,0 +1,165 @@
1
+ # ContentDocs Component
2
+
3
+ ## Overview
4
+
5
+ `ContentDocs` is a docs-page layout shell: a primary nav column, a main content column, and an "on this page" nav column, arranged via CSS `@container` queries (not viewport breakpoints) so the layout adapts to the space actually available — correct even when a consumer page has its own decoration (e.g. a site-wide left nav) eating into the viewport.
6
+
7
+ `docsNav` and `docsPageNav` are **not** slots — they're rendered internally as `ExpandingPanel` accordions driven by `docsNavItems`/`docsPageNavItems` props. Only `docsContent` remains a slot, since it's arbitrary page content. The two side panels auto force-open/collapse based on the component's own measured width via `useContainerBreakpoints`, and share a native `<details name>` accordion group only on mobile (see "Breakpoint behaviour" below).
8
+
9
+ ---
10
+
11
+ ## Props reference
12
+
13
+ | Prop | Type | Default | Notes |
14
+ |------|------|---------|-------|
15
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Root element tag. |
16
+ | `docsNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsNav` panel. Panel (and its `.docs-nav` wrapper) is omitted entirely when empty. |
17
+ | `docsPageNavItems` | `DocsNavItem[]` | `[]` | Items rendered in the `docsPageNav` panel. Panel omitted entirely when empty. |
18
+ | `docsNavLabel` | `string` | `"Navigation"` | Heading text for the `docsNav` panel's `#summary`. |
19
+ | `docsPageNavLabel` | `string` | `"On this page"` | Heading text for the `docsPageNav` panel's `#summary`. |
20
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.content-docs` element. |
21
+
22
+ `DocsNavItem` (from `~/types/components`):
23
+
24
+ ```ts
25
+ interface DocsNavItem {
26
+ label: string;
27
+ to: string;
28
+ icon?: string; // Icon name, e.g. "lucide:rocket"
29
+ }
30
+ ```
31
+
32
+ ## Model
33
+
34
+ | Model | Type | Default | Notes |
35
+ |-------|------|---------|-------|
36
+ | `v-model:activeNavItem` | `string \| undefined` | `undefined` | The `to` of the currently-active `docsNav` item. Updates automatically on click; bind externally (e.g. to route matching) to control it. |
37
+ | `v-model:activePageNavItem` | `string \| undefined` | `undefined` | Same, for `docsPageNav`. |
38
+
39
+ Open/expanded state of the two panels is **not** exposed as a model — it's driven entirely by the container-width breakpoint logic (see below), not user-controllable.
40
+
41
+ ---
42
+
43
+ ## Slots
44
+
45
+ | Slot | Purpose |
46
+ |------|---------|
47
+ | `#docsContent` | Main page content. Only slot this component exposes. |
48
+
49
+ ---
50
+
51
+ ## Usage example
52
+
53
+ ```vue
54
+ <script setup lang="ts">
55
+ import type { DocsNavItem } from "~/types/components";
56
+
57
+ const docsNavItems: DocsNavItem[] = [
58
+ { label: "Getting started", to: "/docs", icon: "lucide:rocket" },
59
+ { label: "Installation", to: "/docs/install" },
60
+ ];
61
+ const docsPageNavItems: DocsNavItem[] = [
62
+ { label: "Overview", to: "/docs#overview" },
63
+ ];
64
+
65
+ const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);
66
+ const activePageNavItem = ref<string | undefined>(undefined);
67
+ </script>
68
+
69
+ <template>
70
+ <ContentDocs
71
+ v-model:active-nav-item="activeNavItem"
72
+ v-model:active-page-nav-item="activePageNavItem"
73
+ :docs-nav-items="docsNavItems"
74
+ :docs-page-nav-items="docsPageNavItems"
75
+ >
76
+ <template #docsContent>
77
+ <h1>Page title</h1>
78
+ <p>Page content.</p>
79
+ </template>
80
+ </ContentDocs>
81
+ </template>
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Breakpoint behaviour
87
+
88
+ Widths are measured on the component's own root element (`useContainerBreakpoints`, container-name `contentDocs`), **not** the viewport — this is deliberate, so nested page decoration (nav rails, sidebars) that shrinks the actual available space is correctly accounted for. Thresholds: `tablet: 768px`, `desktop: 1024px`, matching the `@container contentDocs` queries in this component's own `<style>` block. If you change one, change the other.
89
+
90
+ | Width | `docsNav` | `docsPageNav` |
91
+ |-------|-----------|----------------|
92
+ | < 768px (mobile) | collapsible, closed by default | collapsible, closed by default |
93
+ | 768–1023px (tablet) | collapsible, closed by default | **forced open**, no toggle icon |
94
+ | ≥ 1024px (desktop) | **forced open**, no toggle icon | **forced open**, no toggle icon |
95
+
96
+ ### Why the two panels share a `name` only on mobile
97
+
98
+ Both panels are `ExpandingPanel`s using the native `<details name="...">` grouping feature, which makes same-named panels mutually exclusive (browser force-closes one when the other opens). That's the wanted behaviour on mobile (accordion — only one open at a time), but at tablet/desktop both panels must be open **simultaneously** — a shared name there would make the browser silently force-close one of them the moment both try to be open. So the component computes distinct names (`"docsNav"` / `"docsPageNav"`) once past mobile, and a shared name (`"docsPanelGroup"`) only while mobile.
99
+
100
+ ### Related fix in ExpandingPanel.vue
101
+
102
+ Building this component's forced-open/forced-closed cycling surfaced a real bug in `ExpandingPanel.vue`: `forceOpened` driving the native `open` attribute fires the element's own `toggle` event, which — before the fix — leaked into `isPanelOpen` (the user-click model), leaving the panel permanently "remembered open" even after `forceOpened` reverted to `false`. Fixed by ignoring toggle events while `forceOpened` is `true`. See `expanding-panel.md` and the `ExpandingPanel.spec.ts` regression test ("does not leak forceOpened into isPanelOpen...").
103
+
104
+ ---
105
+
106
+ ## Icons
107
+
108
+ `DocsNavItem.icon` is optional (any icon name resolvable by `<Icon>`, e.g. Lucide set: `"lucide:rocket"`). Items without an icon still align correctly with icon-bearing items — the link is `display: grid` with a fixed-width icon column (`--docs-nav-link-icon-size`), not `flex`, so an absent icon doesn't collapse the label leftward.
109
+
110
+ To move the icon to the end of the link instead of the start, set `--docs-nav-link-icon-order` (or `--docs-page-nav-link-icon-order`) to `rtl` (default `ltr`). This uses a `direction` flip to mirror which physical side the fixed-width column renders on, rather than swapping `grid-column` values directly — swapping columns would put the label into the icon-sized track and squeeze it. The icon and label content reset `direction: ltr` internally so text/glyphs don't visually mirror.
111
+
112
+ ---
113
+
114
+ ## CSS Token Customization
115
+
116
+ All `--content-docs-*` tokens can be overridden at global, page, or instance scope. Each has a **shared** version (applies to both `docsNav` and `docsPageNav`) and a **per-side** override (`content-docs-nav-*` / `content-docs-page-nav-*`) that falls back to the shared token if unset.
117
+
118
+ **Heading tokens** (shared: `--content-docs-heading-*`, per-side: `--content-docs-{nav,page-nav}-heading-*`):
119
+ - `-font-size`, `-font-weight`, `-color`, `-bg`, `-margin`, `-padding-block`, `-padding-inline`
120
+
121
+ **Panel tokens** (shared: `--content-docs-panel-*`, per-side: `--content-docs-{nav,page-nav}-panel-bg`):
122
+ - `-bg` (default: `light-dark(var(--slate-00), var(--slate-10))`, the project's standard card-surface token), `-padding-block`, `-padding-inline`, `-border-radius`
123
+
124
+ **Link tokens** (shared: `--content-docs-link-*`, per-side: `--content-docs-{nav,page-nav}-link-*`):
125
+ - `-font-size`, `-padding-block`, `-padding-inline`, `-margin-block`, `-border-radius`, `-color`, `-bg`, `-hover-bg`, `-hover-color`, `-active-bg`, `-active-color`
126
+
127
+ **Column-width tokens** (fixed-width grid tracks at tablet/desktop):
128
+ - `--content-docs-nav-column-width` (default `23rem`, desktop `docsNav` track)
129
+ - `--content-docs-page-nav-column-width` (default `22rem`, desktop `docsPageNav` track)
130
+ - `--content-docs-page-nav-column-width-tablet` (default `20rem`, tablet's single fixed track — `docsNav` is full-width at tablet)
131
+
132
+ **Icon tokens** (not `content-docs-` prefixed — shared with the link, not per-side by default):
133
+ - `--docs-nav-link-icon-gap`, `--docs-nav-link-icon-size`, `--docs-nav-link-icon-order` (`ltr`/`rtl`), `--docs-page-nav-link-icon-order`
134
+
135
+ ---
136
+
137
+ ## Local style override scaffold
138
+
139
+ ```vue
140
+ <ContentDocs :style-class-passthrough="['my-docs']" ...>
141
+ ...
142
+ </ContentDocs>
143
+
144
+ <style>
145
+ .content-docs {
146
+ &.my-docs {
147
+ --content-docs-panel-bg: var(--surface-2);
148
+ --content-docs-link-active-bg: var(--brand-01);
149
+ --content-docs-link-active-color: var(--brand-10);
150
+ }
151
+ }
152
+ </style>
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Notes
158
+
159
+ - `docsContent` visibility is slot-detected (`useSlots().docsContent`); `docsNav`/`docsPageNav` visibility is item-array-length-detected (`docsNavItems.length > 0`) — different mechanisms, since only `docsContent` is still a real slot.
160
+ - `NuxtLink` resolved via `resolveComponent("NuxtLink")`, not imported from `#components` — required so this component works inside Storybook (see `feedback_no_components_import_storybook`).
161
+ - Auto-imported in Nuxt — no manual import needed.
162
+ - File: `app/components/01.atoms/content-wrappers/docs-pages/ContentDocs.vue`
163
+ - Types: `app/types/components/content-docs.d.ts` (`DocsNavItem`)
164
+ - Tests: `app/components/01.atoms/content-wrappers/docs-pages/tests/ContentDocs.spec.ts`
165
+ - Demo page: `app/pages/ui/layout-content-docs.vue`
@@ -13,6 +13,7 @@
13
13
  | `name` | `string` | `useId()` | Identifies the panel. Used in ARIA attributes (`id-{name}-trigger`, `id-{name}-content`). If omitted, a unique id is generated automatically. |
14
14
  | `animationDuration` | `number` | `400` | Expand/collapse transition duration in milliseconds. Pass `0` to disable animation. |
15
15
  | `forceOpened` | `boolean` | `false` | When `true`, the panel is always open. The toggle icon is hidden and clicks do not close the panel. |
16
+ | `contentIsOnTop` | `boolean` | `false` | When `true`, the content region is taken out of flow and absolutely positioned directly below the summary, raised above surrounding page content via `z-index` — instead of pushing layout down when it opens. Applies a `content-is-on-top` class to the root `.expanding-panel` element (not the content div). |
16
17
  | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra CSS classes applied to the root `.expanding-panel` element. |
17
18
 
18
19
  ## Model
@@ -98,6 +99,29 @@ const isOpen = ref(false);
98
99
  </ExpandingPanel>
99
100
  ```
100
101
 
102
+ ### Content on top (overlay instead of pushing layout)
103
+
104
+ ```vue
105
+ <ExpandingPanel name="overlay" :content-is-on-top="true" :style-class-passthrough="['my-overlay-panel']">
106
+ <template #summary><span>Open me — content overlays what's below</span></template>
107
+ <template #content>
108
+ <!-- Wrapper INSIDE the slot carries the visual styling — see
109
+ "Styling the content when contentIsOnTop" below for why it can't go on .inner -->
110
+ <div class="my-overlay-panel-body">
111
+ <p>Positioned absolutely below the summary, doesn't push page content down.</p>
112
+ </div>
113
+ </template>
114
+ </ExpandingPanel>
115
+
116
+ <style>
117
+ .my-overlay-panel-body {
118
+ background-color: white;
119
+ padding: 1rem;
120
+ box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
121
+ }
122
+ </style>
123
+ ```
124
+
101
125
  ---
102
126
 
103
127
  ## ARIA / accessibility
@@ -117,6 +141,36 @@ Always supply a meaningful `name` prop when using multiple panels on the same pa
117
141
 
118
142
  ---
119
143
 
144
+ ## Styling the content when contentIsOnTop
145
+
146
+ When `contentIsOnTop` is `true`, the component deliberately does **not** set `background-color`, `padding`, or a shadow on `.inner` — and consumers must not set them on `.inner` either. Always style a wrapper element placed *inside* the `#content` slot (see example above).
147
+
148
+ Why: `.expanding-panel-content` collapses via `grid-template-rows: 0fr → 1fr`, and `.inner` relies on `overflow: hidden` on its own box to clip its *children* to 0px when collapsed. `overflow: hidden` clips overflow content, but does not shrink the element's own padding/border/background — those are part of `.inner`'s own box model and still render at full size even while the row track is `0fr` and the panel is closed, producing a visible gap under the summary. A wrapper placed inside the slot is a *child* of `.inner`, so its box — including any padding/background/shadow — is correctly clipped to 0px by `.inner`'s `overflow: hidden` while closed. Baking styling into `.inner` itself would require also gating it on the open state (e.g. `.expanding-panel-details[open] ~ .expanding-panel-content .inner`), which is unnecessary complexity — styling the slot content is the correct fix, not a workaround.
149
+
150
+ ---
151
+
152
+ ## Constraint: don't stack contentIsOnTop panels as direct siblings
153
+
154
+ `contentIsOnTop` takes the content out of document flow (`position: absolute`) specifically so opening the panel does **not** push whatever comes after it down the page — that's the entire point of the prop. The tradeoff: the panel's own container still only occupies the height of its `<summary>` row, so a *sibling* element positioned directly after it in the DOM sits exactly where the overlay renders. When that sibling is another `ExpandingPanel`, opening the first one visually covers the second one's summary — this happens whether or not the two are grouped via a shared `name` (linked accordion) or opened simultaneously; it isn't specific to linking.
155
+
156
+ `contentIsOnTop` is designed for a **single** panel overlaying unrelated trailing page content (e.g. a promo banner, a footer strip) — not for stacking multiple `contentIsOnTop` panels beside each other expecting normal accordion behaviour. If you need several linked/stacked panels, leave `contentIsOnTop` off (the default, in-flow layout handles that case correctly).
157
+
158
+ ---
159
+
160
+ ## CSS Token Customization
161
+
162
+ All `--expanding-panel-*` tokens can be overridden at global, page, or instance scope. See `CONSUMER-STYLING.md` in the component directory for full token documentation and examples.
163
+
164
+ **Tokens:**
165
+
166
+ - `--expanding-panel-summary-gap` — gap between summary label and icon
167
+ - `--expanding-panel-summary-padding-block` — summary row vertical padding
168
+ - `--expanding-panel-icon-size` — toggle icon size
169
+ - `--expanding-panel-content-z-index` — stacking order when `contentIsOnTop` is `true`
170
+ - `--expanding-panel-content-gap` — space between summary and content when `contentIsOnTop` is `true`
171
+
172
+ ---
173
+
120
174
  ## Local style override scaffold
121
175
 
122
176
  When consuming this component, scaffold a style block using `styleClassPassthrough`. Delete the block if unused.
@@ -150,6 +204,7 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
150
204
  ## Notes
151
205
 
152
206
  - The open/close animation uses `grid-template-rows: 0fr → 1fr` — no JS height measurement needed.
207
+ - `content-is-on-top` is applied to the root `.expanding-panel` element, not `.expanding-panel-content` — style overrides must scope through it, e.g. `.expanding-panel.my-panel .expanding-panel-content .inner { ... }`.
153
208
  - When `forceOpened` is `true`, `open` stays `true` regardless of `v-model`, but `v-model` still updates internally on clicks (useful if you later set `forceOpened` back to `false`).
154
209
  - Group panels into a native accordion (only one open at a time) by passing the same `name` to multiple panels or use `AccordianCore` which handles this automatically.
155
210
  - Auto-imported in Nuxt — no manual import needed.
@@ -28,6 +28,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
28
28
  ├── testing-add-playwright.md — create a Playwright visual regression test
29
29
  ├── setup-postinstall.md — automate nuxt prepare + Claude skills copy via postinstall so neither is forgotten after npm install
30
30
  ├── theming-colour-ramps.md — parametric oklch ramp system: formula, named palettes, semantic slots, generator, consumer setup
31
+ ├── theming-typography-tokens.md — --step-N fluid font-size scale, 62.5% root reset gotcha (1rem=10px, not 16px), page-heading-*/page-body-* utility classes
31
32
  ├── theming-override-default.md — replace the entire default theme with a custom palette (set --theme-hue/--theme-chroma)
32
33
  ├── theming-partial-override.md — override a specific token category (palette, buttons, inputs) without a full theme replacement
33
34
  ├── colour-scheme-disable.md — disable light/dark scheme support in a consumer app
@@ -45,7 +46,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
45
46
  ├── vercel-node-version.md — .nvmrc pinned to Node 24 is required; without it Vercel uses npm 10 which crashes on versionless optional stubs
46
47
  ├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
47
48
  ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
48
- ├── qa-panel.md — collapsible dev-only panel for toggling component props live on a page (demo pages and consuming apps)
49
+ ├── qa-panel.md — collapsible panel for toggling component props live on a page (always on for /pages/ui/ demo pages; gate with isDev in consuming apps)
49
50
  ├── release-notes.md — produce release notes as a fenced markdown block from git log
50
51
  ├── pull-request-description.md — produce a PR description as a fenced markdown block from git diff vs main
51
52
  ├── using-component-skills.md — discover and use component skills in consumer apps: where skills land, browsing patterns, workflow for deciding build vs. compose
@@ -71,7 +72,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
71
72
  ├── services-section.md — ServicesSection props (incl. heading/CTA copy), summary-link/cta/cta-panel slots, summary vs full mode
72
73
  ├── contact-section.md — ContactSection props (stepperIndicatorSize pass-through), 3-item info+form layout, slot API
73
74
  ├── stepper-list.md — StepperList dynamic slots (item-{n}/indicator-{n}), props, connector behaviour
74
- ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, slots (summary/icon/content), ARIA wiring
75
+ ├── expanding-panel.md — ExpandingPanel v-model, forceOpened, contentIsOnTop overlay mode, slots (summary/icon/content), ARIA wiring, CSS token API
76
+ ├── 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
75
77
  ├── glass-panel.md — GlassPanel props, slots, CSS token API (--glass-panel-bg/border-color/shadow/highlight), theming override
76
78
  ├── navigation-horizontal.md — NavigationHorizontal props, NavItemData type, CSS token API, import path gotcha
77
79
  ├── pricing-card.md — PricingCard: SaaS-style plan card with highlight, feature list, #cta slot for button customization, CSS token API
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Overview
4
4
 
5
- A collapsible dev-only panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Hidden in production via `import.meta.dev`. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed. Useful on both demo pages in this library and on pages in consuming apps.
5
+ A collapsible panel that lets you toggle component props live on a page — without touching the component or breaking the visual layout. Uses a native `<details>`/`<summary>` so it takes up no space when collapsed. Every page under `app/pages/ui/` in this library is a demo-only page already, so the panel always renders there — no `isDev` gate needed. If you add one to a page in a consuming app that also serves real production traffic, gate it behind that app's own dev/staging check.
6
6
 
7
7
  ## Structure
8
8
 
@@ -19,8 +19,7 @@ Each group controls one prop. The status `<code>` in the summary mirrors the cur
19
19
  ### 1. Add the reactive state to `<script setup>`
20
20
 
21
21
  ```ts
22
- // ── QA controls (dev only) ────────────────────────────────────────
23
- const isDev = import.meta.dev;
22
+ // ── QA controls ────────────────────────────────────────────────────
24
23
 
25
24
  // One ref per controllable prop
26
25
  const qaMyBoolean = ref(true);
@@ -47,8 +46,8 @@ const myStringPresets = ["a", "b", "c"] as const;
47
46
  Place directly above (or below) the component being QA'd, outside any layout wrapper that clips content:
48
47
 
49
48
  ```vue
50
- <!-- ── QA Panel (dev only) ───────────────────────────────── -->
51
- <div v-if="isDev" class="qa-panel">
49
+ <!-- ── QA Panel ─────────────────────────────────────────────── -->
50
+ <div class="qa-panel">
52
51
  <details class="qa-panel__details">
53
52
  <summary class="qa-panel__summary">
54
53
  <span class="qa-panel__title">QA — MyComponent</span>
@@ -219,7 +218,7 @@ Scope inside your page body class (e.g. `.my-page`) so styles don't bleed. The p
219
218
 
220
219
  ## Notes
221
220
 
222
- - **Production safety**: `import.meta.dev` is `false` in production builds the entire `v-if="isDev"` block is tree-shaken. No runtime cost.
221
+ - **Consuming apps on real production traffic**: if the page hosting the panel isn't demo-only (unlike everything under `app/pages/ui/` in this library), gate the panel with that app's own dev/staging check, e.g. `v-if="isDev"` with `const isDev = import.meta.dev;`. `import.meta.dev` is `false` in production builds, so the gated block is tree-shaken with no runtime cost.
223
222
  - **Consuming apps**: The active chip color (`oklch(55% 0.18 240)`) is a neutral blue. Replace with a brand accent token if preferred: `background: var(--color-brand-accent)`.
224
223
  - **Panel placement**: Outside any `overflow: hidden` or clipping container, otherwise the panel may be clipped or push layout unexpectedly. Placing it as a direct sibling of the component row works well.
225
224
  - **Computed CSS vars**: When a prop controls a CSS custom property (e.g. max-height tiers), use a `computed` that returns a style object and bind it with `:style` on the component wrapper:
@@ -0,0 +1,109 @@
1
+ # Typography Tokens
2
+
3
+ ## Overview
4
+
5
+ `html { font-size: 62.5%; }` is set globally by this layer (`setup/01.config/_head.css`), which
6
+ redefines `1rem` to `10px` instead of the browser default `16px`. Every font-size in this design
7
+ system is calibrated against that 10px root, via the fluid `--step-N` custom properties and the
8
+ `page-heading-*`/`page-body-*` utility classes built on top of them — never a raw `rem` literal.
9
+
10
+ **The trap**: a raw value like `font-size: 0.85rem` looks like a reasonable ~13.6px in a normal
11
+ 16px-root project. In a consumer of this layer it renders at 8.5px — because `1rem` here is `10px`,
12
+ not `16px`. This silently produces illegibly small text with no error or warning; it only shows up
13
+ visually. Always use `var(--step-N)` (or a `page-heading-*`/`page-body-*` utility class) instead of
14
+ a raw `rem`/`px` font-size when writing new component or page-local CSS in a consumer app.
15
+
16
+ ## The token scale
17
+
18
+ Defined in `setup/05.typography/01.tokens/_reponsive-font-sizes.css`, on `:where(html)`. Each step
19
+ is a fluid `clamp()` — smallest at narrow viewports, largest at wide ones:
20
+
21
+ | Token | Approx. range (px, at 10px root) |
22
+ |---|---|
23
+ | `--step-1` | ~7.8–8 |
24
+ | `--step-2` | ~9.4–10 |
25
+ | `--step-3` | ~11.25–12.5 |
26
+ | `--step-4` | ~13.5–15.6 (body default) |
27
+ | `--step-5` | ~16.2–19.5 |
28
+ | `--step-6` | ~19.4–24.4 |
29
+ | `--step-7` | ~23.3–30.5 |
30
+ | `--step-8` | ~28–38.1 |
31
+ | `--step-9` | ~33.6–47.7 |
32
+ | `--step-10` | ~40.3–59.6 |
33
+
34
+ Plus purpose-specific tokens for hero/eyebrow text: `--hero-text-display`, `--hero-text-title`,
35
+ `--hero-text-heading`, `--hero-text-subheading`, `--hero-text-label`, `--eyebrow-text-large`,
36
+ `--eyebrow-text-medium`, `--eyebrow-text-small`.
37
+
38
+ `body` itself is set to `font-size: var(--step-4)` (`setup/01.config/_head.css`), so plain `<p>`/`<li>`
39
+ text inherits the correct scaled size automatically — you only need to reach for a token explicitly
40
+ when setting a font-size on something other than ambient body text (labels, nav links, table cells,
41
+ code blocks, etc.).
42
+
43
+ ## Utility classes built on the scale
44
+
45
+ `setup/05.typography/02.utility-classes/_font-classes-page-heading.css` and
46
+ `_font-classes-page-body.css` map the steps to semantic classes:
47
+
48
+ | Class | Token |
49
+ |---|---|
50
+ | `.page-heading-1` | `--step-8` |
51
+ | `.page-heading-2` | `--step-7` |
52
+ | `.page-heading-3` | `--step-6` |
53
+ | `.page-heading-4` | `--step-5` |
54
+ | `.page-heading-5` | `--step-4` |
55
+ | `.page-heading-6` | `--step-3` |
56
+ | `.page-body-large` | `--step-6` |
57
+ | `.page-body-medium` | `--step-5` |
58
+ | `.page-body-normal` | `--step-4` |
59
+ | `.page-body-small` | `--step-3` |
60
+ | `.page-body-xsmall` | `--step-2` |
61
+
62
+ `page-body-*` also has `-light`/`-semibold`/`-bold` weight variants (e.g. `.page-body-normal-bold`).
63
+
64
+ ## Steps
65
+
66
+ ### 1. Prefer the utility classes for standalone text
67
+
68
+ If a heading or paragraph in your markup can just be a plain element with one of these classes,
69
+ use the class rather than writing new CSS:
70
+
71
+ ```vue
72
+ <h1 class="page-heading-1">Dashboard</h1>
73
+ <p class="page-body-normal">Overview of your account activity.</p>
74
+ ```
75
+
76
+ ### 2. Use `var(--step-N)` inside component-scoped CSS
77
+
78
+ For font-sizes inside a component's own `<style>` block (sidebar labels, table text, code blocks,
79
+ badges, etc.), reference the token directly rather than a class:
80
+
81
+ ```css
82
+ .my-component__label {
83
+ font-size: var(--step-4);
84
+ }
85
+ ```
86
+
87
+ Don't go below `--step-4` for anything a user is expected to actually read (nav links, table
88
+ cells, code, captions) — `--step-2`/`--step-3` look reasonable in isolation but read as too small
89
+ once placed in real dense UI (sidebars, TOCs, code blocks) next to normal body text. Reserve
90
+ `--step-1`–`--step-3` for genuinely decorative/secondary marks (eyebrow labels, badge counters)
91
+ where legibility isn't the point.
92
+
93
+ ### 3. Pick a step by relative scale, not by trying to hit a literal px target
94
+
95
+ Since every step is fluid (`clamp()`), don't reason about "I want 12px" — reason about it relative
96
+ to body text (`--step-4`): one step down (`--step-3`) for slightly smaller text (small labels, table
97
+ cells), two steps down (`--step-2`) for compact caption/label text (sidebar nav group titles, tab
98
+ labels), and so on upward for headings.
99
+
100
+ ## Notes
101
+
102
+ - This applies to *any* consumer app extending this layer, not just components inside this repo —
103
+ a local/custom component written in a consuming app (e.g. a docs page's own sidebar/TOC component)
104
+ needs the same `var(--step-N)` treatment, since it inherits the same `62.5%` root reset via the
105
+ layer's global CSS.
106
+ - Caught in the wild: a docs-page sidebar/TOC/code-block set of components in a consumer app
107
+ (`guidemyhair`) shipped with raw `0.75rem`/`0.85rem`/`0.9rem` font-sizes, assuming a 16px root —
108
+ rendered far too small once the layer's `62.5%` reset applied. Fixed by switching every one to the
109
+ matching `--step-N` token.
@@ -0,0 +1,129 @@
1
+ {
2
+ "SRCDEV ContentDocs Basic": {
3
+ "description": "ContentDocs with nav + on-this-page items and active-item v-models",
4
+ "scope": "vue,html",
5
+ "body": [
6
+ "<ContentDocs",
7
+ " v-model:active-nav-item=\"$1activeNavItem\"",
8
+ " v-model:active-page-nav-item=\"$2activePageNavItem\"",
9
+ " :docs-nav-items=\"$3docsNavItems\"",
10
+ " :docs-page-nav-items=\"$4docsPageNavItems\"",
11
+ ">",
12
+ " <template #docsContent>",
13
+ " <p>$5Page content goes here.</p>",
14
+ " </template>",
15
+ "</ContentDocs>"
16
+ ]
17
+ },
18
+ "SRCDEV ContentDocs Custom Labels" : {
19
+ "description": "ContentDocs with custom docsNav / docsPageNav panel headings",
20
+ "scope": "vue,html",
21
+ "body": [
22
+ "<ContentDocs",
23
+ " v-model:active-nav-item=\"$1activeNavItem\"",
24
+ " v-model:active-page-nav-item=\"$2activePageNavItem\"",
25
+ " :docs-nav-items=\"$3docsNavItems\"",
26
+ " :docs-page-nav-items=\"$4docsPageNavItems\"",
27
+ " docs-nav-label=\"$5Sections\"",
28
+ " docs-page-nav-label=\"$6Contents\"",
29
+ ">",
30
+ " <template #docsContent>",
31
+ " <p>$7Page content goes here.</p>",
32
+ " </template>",
33
+ "</ContentDocs>"
34
+ ]
35
+ },
36
+ "SRCDEV ContentDocs Items Script": {
37
+ "description": "docsNavItems / docsPageNavItems + active-item refs for ContentDocs",
38
+ "scope": "typescript,vue",
39
+ "body": [
40
+ "const docsNavItems: DocsNavItem[] = [",
41
+ " { label: \"$1Getting started\", to: \"$2/docs\" },",
42
+ "];",
43
+ "",
44
+ "const docsPageNavItems: DocsNavItem[] = [",
45
+ " { label: \"$3Overview\", to: \"$2/docs#overview\" },",
46
+ "];",
47
+ "",
48
+ "const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);",
49
+ "const activePageNavItem = ref<string | undefined>(undefined);"
50
+ ]
51
+ },
52
+ "SRCDEV ContentDocs Items With Icons Script": {
53
+ "description": "docsNavItems / docsPageNavItems with per-item icons for ContentDocs",
54
+ "scope": "typescript,vue",
55
+ "body": [
56
+ "const docsNavItems: DocsNavItem[] = [",
57
+ " { label: \"$1Getting started\", to: \"$2/docs\", icon: \"$3lucide:rocket\" },",
58
+ "];",
59
+ "",
60
+ "const docsPageNavItems: DocsNavItem[] = [",
61
+ " { label: \"$4Overview\", to: \"$2/docs#overview\", icon: \"$5lucide:list\" },",
62
+ "];",
63
+ "",
64
+ "const activeNavItem = ref<string | undefined>(docsNavItems[0]?.to);",
65
+ "const activePageNavItem = ref<string | undefined>(undefined);"
66
+ ]
67
+ },
68
+ "SRCDEV ContentDocs CSS Override — Icons": {
69
+ "description": "CSS override scaffold for ContentDocs nav-link icon tokens (gap, size, and start/end order)",
70
+ "scope": "css",
71
+ "body": [
72
+ ".$1my-content-docs {",
73
+ " .content-docs {",
74
+ " --docs-nav-link-icon-gap: $2;",
75
+ " --docs-nav-link-icon-size: $3;",
76
+ " --docs-nav-link-icon-order: $4rtl; /* icon at the end */",
77
+ " --docs-page-nav-link-icon-order: $5ltr; /* icon at the start (default) */",
78
+ " }",
79
+ "}"
80
+ ]
81
+ },
82
+ "SRCDEV ContentDocs CSS Override — Shared Theme": {
83
+ "description": "CSS override scaffold for ContentDocs heading/panel/link tokens, applied to both docsNav and docsPageNav at once",
84
+ "scope": "css",
85
+ "body": [
86
+ ".$1my-content-docs {",
87
+ " .content-docs {",
88
+ " --content-docs-heading-font-size: $2;",
89
+ " --content-docs-heading-color: $3;",
90
+ " --content-docs-heading-bg: $4;",
91
+ " --content-docs-heading-padding-block: $5;",
92
+ "",
93
+ " --content-docs-panel-bg: $6;",
94
+ " --content-docs-panel-padding-block: $7;",
95
+ " --content-docs-panel-padding-inline: $8;",
96
+ "",
97
+ " --content-docs-link-font-size: $9;",
98
+ " --content-docs-link-color: $10;",
99
+ " --content-docs-link-hover-bg: $11;",
100
+ " --content-docs-link-active-bg: $12;",
101
+ " --content-docs-link-active-color: $13;",
102
+ "",
103
+ " --content-docs-nav-column-width: $14;",
104
+ " --content-docs-page-nav-column-width: $15;",
105
+ " --content-docs-page-nav-column-width-tablet: $16;",
106
+ " }",
107
+ "}"
108
+ ]
109
+ },
110
+ "SRCDEV ContentDocs CSS Override — Per Side": {
111
+ "description": "CSS override scaffold for independently theming docsNav vs docsPageNav heading/panel/link tokens",
112
+ "scope": "css",
113
+ "body": [
114
+ ".$1my-content-docs {",
115
+ " .content-docs {",
116
+ " /* docsNav (primary) */",
117
+ " --content-docs-nav-heading-color: $2;",
118
+ " --content-docs-nav-panel-bg: $3;",
119
+ " --content-docs-nav-link-active-bg: $4;",
120
+ "",
121
+ " /* docsPageNav (on-this-page / secondary) */",
122
+ " --content-docs-page-nav-heading-color: $5;",
123
+ " --content-docs-page-nav-panel-bg: $6;",
124
+ " --content-docs-page-nav-link-active-bg: $7;",
125
+ " }",
126
+ "}"
127
+ ]
128
+ }
129
+ }