srcdev-nuxt-components 9.1.37 → 9.1.39

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 (28) hide show
  1. package/.claude/settings.json +4 -1
  2. package/.claude/skills/components/carousel-flip.md +188 -0
  3. package/.claude/skills/components/data-grid.md +167 -0
  4. package/.claude/skills/index.md +4 -1
  5. package/.claude/skills/qa-panel.md +231 -0
  6. package/app/components/01.atoms/grids/data-grid/DataGrid.vue +39 -0
  7. package/app/components/01.atoms/grids/data-grid/stories/DataGrid.stories.ts +234 -0
  8. package/app/components/01.atoms/grids/data-grid/tests/DataGrid.spec.ts +140 -0
  9. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +11 -0
  10. package/app/components/01.atoms/{grid-stack → grids/grid-stack}/stories/GridStack.stories.ts +1 -1
  11. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +49 -49
  12. package/app/components/carousels/CarouselFlip.vue +211 -153
  13. package/app/components/carousels/stories/CarouselFlip.stories.ts +35 -0
  14. package/app/components/carousels/tests/CarouselFlip.spec.ts +38 -0
  15. package/app/pages/ui/carousel-flip.vue +222 -17
  16. package/app/pages/ui/simple-grid.vue +2 -2
  17. package/package.json +1 -1
  18. package/app/components/display-grid/DisplayGridCore.vue +0 -22
  19. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/ScrollRevealFrame.vue +0 -0
  20. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/stories/ScrollRevealFrame.stories.ts +0 -0
  21. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/ScrollRevealFrame.spec.ts +0 -0
  22. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +0 -0
  23. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/ScrollRevealImage.vue +0 -0
  24. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/stories/ScrollRevealImage.stories.ts +0 -0
  25. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/ScrollRevealImage.spec.ts +0 -0
  26. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +0 -0
  27. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/GridStack.vue +0 -0
  28. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/tests/GridStack.spec.ts +0 -0
@@ -23,7 +23,10 @@
23
23
  "Bash(npm run:*)",
24
24
  "Bash(git add:*)",
25
25
  "Bash(gh release:*)",
26
- "Bash(node -e ':*)"
26
+ "Bash(node -e ':*)",
27
+ "Bash(git ls-tree *)",
28
+ "Bash(node -p \"require\\('./package.json'\\).version\")",
29
+ "Bash(git status *)"
27
30
  ],
28
31
  "additionalDirectories": []
29
32
  }
@@ -0,0 +1,188 @@
1
+ # CarouselFlip
2
+
3
+ ## Overview
4
+
5
+ A FLIP-animated carousel that reorders items in the DOM using CSS `order` and animates transitions with the FLIP technique (First, Last, Invert, Play). Supports swipe, keyboard navigation, and marker dots. The prev/next buttons and controls bar can be placed in several layouts via a single prop.
6
+
7
+ ---
8
+
9
+ ## Implementation guide
10
+
11
+ When a dev asks to implement CarouselFlip, work through the following questions before writing any code. Each answer maps directly to a prop or CSS decision. You do not need to ask all questions at once — use context clues where the answer is obvious.
12
+
13
+ ### Step 1 — Data source
14
+
15
+ Ask: **"Where is the carousel data coming from — a static array, an API, or a Nuxt `useFetch`?"**
16
+
17
+ - **Static array**: Define `carouselDataIds` directly as a `const`.
18
+ - **API / useFetch**: Derive `carouselDataIds` as a `computed` from the response. Gate the component with `v-if="status === 'success'"` to avoid a flash of empty slots.
19
+
20
+ ```vue
21
+ <!-- API pattern -->
22
+ const { data, status } = await useFetch<MyType>("/api/items");
23
+ const carouselDataIds = computed(() => data.value?.items.map(i => i.id) ?? []);
24
+
25
+ <CarouselFlip v-if="status === 'success'" :carousel-data-ids="carouselDataIds">
26
+ <template v-for="item in data.items" :key="item.id" #[item.id]>
27
+ <!-- slot content -->
28
+ </template>
29
+ </CarouselFlip>
30
+ ```
31
+
32
+ ### Step 2 — Button layout
33
+
34
+ Ask: **"Where should the prev/next buttons sit?"** Show these options:
35
+
36
+ | Value | Visual description |
37
+ |---|---|
38
+ | `"sides"` | Buttons float on the left and right edges of the carousel frame, centred vertically — the classic look |
39
+ | `"controls-flanking"` | Buttons move down into the controls row: prev · markers · next |
40
+ | `"controls-grouped-right"` | Markers stretch left, both buttons grouped together at the far right of the controls row |
41
+ | `"overlay"` | Buttons stay on the sides; the markers bar overlays the bottom edge of the carousel frame |
42
+
43
+ Set via `:button-layout="..."`. Default is `"sides"`.
44
+
45
+ ### Step 3 — Show or hide the markers bar
46
+
47
+ Ask: **"Do you want the dot/marker navigation bar visible?"**
48
+
49
+ - **Yes** (default): omit the prop or pass `:show-controls="true"`.
50
+ - **No**: pass `:show-controls="false"`. The element is fully removed from the DOM — no layout space, no keyboard listener.
51
+
52
+ > If `showControls` is false and `buttonLayout` is `"controls-flanking"` or `"controls-grouped-right"`, the controls row collapses entirely — only the buttons remain. Recommend switching `buttonLayout` to `"sides"` or `"overlay"` in that case.
53
+
54
+ ### Step 4 — Edge peek (overflow)
55
+
56
+ Ask: **"Should adjacent carousel items peek in from the sides?"**
57
+
58
+ - **No peek** (default): `:allow-carousel-overflow="false"` — clean, contained look.
59
+ - **Peek visible**: `:allow-carousel-overflow="true"` — items bleed slightly out of the container. Requires CSS custom properties to control how much:
60
+
61
+ ```css
62
+ .my-carousel.carousel-flip {
63
+ --_carousel-item-track-gap: 16px;
64
+ --_carousel-item-edge-preview-width: 32px; /* keep at 2× track-gap */
65
+ --_carousel-container-max-inline-size: 900px;
66
+ }
67
+ ```
68
+
69
+ ### Step 5 — Animation style
70
+
71
+ Ask: **"What animation feel are you going for?"**
72
+
73
+ | Prop | Effect |
74
+ |---|---|
75
+ | `:use-flip-animation="false"` (default) | Slides — items translate horizontally |
76
+ | `:use-flip-animation="true"` | FLIP reorder — items swap position with a physics-aware delta animation |
77
+ | `:use-spring-effect="true"` | Adds spring easing to FLIP transitions. Requires `var(--spring-easing)` in the theme |
78
+ | `:transition-speed="400"` | Duration in ms. Default `200`. Recommended range: `200`–`1200` |
79
+
80
+ ### Step 6 — Minimal working implementation
81
+
82
+ Once the above decisions are made, assemble the component:
83
+
84
+ ```vue
85
+ <CarouselFlip
86
+ :carousel-data-ids="carouselDataIds"
87
+ :allow-carousel-overflow="true"
88
+ :transition-speed="600"
89
+ :use-flip-animation="true"
90
+ :use-spring-effect="false"
91
+ button-layout="sides"
92
+ :show-controls="true"
93
+ :style-class-passthrough="['my-carousel']"
94
+ >
95
+ <template v-for="item in items" :key="item.id" #[item.id]>
96
+ <div class="my-carousel__item">
97
+ <!-- item content -->
98
+ </div>
99
+ </template>
100
+ </CarouselFlip>
101
+ ```
102
+
103
+ ### Step 7 — Style the component
104
+
105
+ Always scope overrides using `styleClassPassthrough` + a page/section wrapper class. Required tokens for a usable carousel:
106
+
107
+ ```css
108
+ .my-carousel.carousel-flip {
109
+ --_carousel-item-track-gap: 16px;
110
+ --_carousel-container-max-inline-size: 900px;
111
+ --_carousel-item-edge-preview-width: 32px; /* 2× track-gap when overflow is on */
112
+
113
+ /* Items */
114
+ .item {
115
+ border-radius: 1.2rem;
116
+ overflow: hidden;
117
+ }
118
+
119
+ /* Markers */
120
+ .btn-marker {
121
+ width: 10px;
122
+ height: 10px;
123
+ border-radius: 100vw;
124
+ background: oklch(70% 0 0);
125
+
126
+ &.active { background: white; }
127
+ }
128
+
129
+ /* Prev/next buttons */
130
+ .btn-action {
131
+ padding: 10px;
132
+ background: oklch(0% 0 0 / 0.4);
133
+ border: none;
134
+ border-radius: 100vw;
135
+ color: white;
136
+ }
137
+ }
138
+ ```
139
+
140
+ ---
141
+
142
+ ## Props reference
143
+
144
+ | Prop | Type | Default | Description |
145
+ |---|---|---|---|
146
+ | `carouselDataIds` | `string[]` | `[]` | Ordered list of unique IDs — each becomes a named slot |
147
+ | `transitionSpeed` | `number` | `200` | Animation duration in ms |
148
+ | `allowCarouselOverflow` | `boolean` | `false` | Allows peeking items outside the container bounds |
149
+ | `useFlipAnimation` | `boolean` | `false` | Enables FLIP reorder animation on prev/next |
150
+ | `useSpringEffect` | `boolean` | `false` | Uses spring easing (`var(--spring-easing)`) instead of `ease` |
151
+ | `buttonLayout` | `"sides" \| "controls-flanking" \| "controls-grouped-right" \| "overlay"` | `"sides"` | Controls placement of prev/next buttons relative to the carousel frame and controls bar |
152
+ | `showControls` | `boolean` | `true` | Show or hide the markers/controls bar. When `false` the element is removed from the DOM; `controlsContainerRef` becomes null and its keyboard listener detaches automatically |
153
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Classes applied to the root element |
154
+
155
+ ## CSS custom properties
156
+
157
+ | Property | Purpose |
158
+ |---|---|
159
+ | `--_carousel-item-track-gap` | Gap between carousel items (default `10px`) |
160
+ | `--_carousel-container-max-inline-size` | Max width of the visible carousel window |
161
+ | `--_carousel-item-edge-preview-width` | How much of adjacent items to reveal (edge peek). Keep at `2× --_carousel-item-track-gap` |
162
+ | `--_carousel-display-max-width` | Max width of the whole component inc. controls |
163
+
164
+ ## HTML structure
165
+
166
+ ```text
167
+ section.carousel-flip ← grid root, data-button-layout="..."
168
+ div.item-container ← grid-area: carousel — flex row of items
169
+ div.item[data-id] ← one per carouselDataIds entry
170
+ div.controls-container ← grid-area: controls — markers bar (v-if="showControls")
171
+ div.markers-container
172
+ ul.markers-list
173
+ li.markers-item
174
+ button.btn-marker
175
+ div.buttons-container ← display: contents by default (transparent to grid)
176
+ button.btn-action.btn-prev ← grid-area: prev (row 1, col 1)
177
+ button.btn-action.btn-next ← grid-area: next (row 1, col 3)
178
+ ```
179
+
180
+ `buttons-container` uses `display: contents` so `.btn-prev`/`.btn-next` participate directly in the parent grid. In the `controls-grouped-right` variant the component sets `display: flex` on it, making it the grid child instead.
181
+
182
+ ## Notes
183
+
184
+ - **Opacity fade-in**: The root starts at `opacity: 0` and gets `.mounted` (opacity 1) after `initialSetup()` completes. This prevents a flash of unstyled layout on mount.
185
+ - **z-index**: `.btn-prev`/`.btn-next` have `z-index: 1` to sit above `.item-container` which uses `isolation: isolate` (carousel items are translated and can overlap the button columns).
186
+ - **ResizeObserver**: `initialSetup()` re-runs on resize to recalculate item widths. CSS `translate` on `.item` is driven by the measured `itemWidth` via `v-bind`.
187
+ - **Spring easing**: `useSpringEffect` switches to `var(--spring-easing)`. Make sure this custom property is defined in your theme or global CSS when enabling it.
188
+ - **buttonLayout + showControls combo**: `"controls-flanking"` and `"controls-grouped-right"` place buttons in the controls row. If `showControls` is false that row collapses — use `"sides"` or `"overlay"` instead.
@@ -0,0 +1,167 @@
1
+ # DataGrid Component
2
+
3
+ ## Overview
4
+
5
+ `DataGrid` is a responsive auto-fit CSS grid wrapper. It renders whatever named slots the consumer provides, auto-fitting columns to a minimum of `250px` each. Column count and gap are controlled via CSS custom properties, making layout adjustments a single-line style override rather than a prop change.
6
+
7
+ ---
8
+
9
+ ## Slot pattern
10
+
11
+ Pass any number of named slots — the component renders each one in document order inside the grid.
12
+
13
+ ```vue
14
+ <DataGrid>
15
+ <template #item-1><StatCard label="Revenue" value="£24,500" /></template>
16
+ <template #item-2><StatCard label="Clients" value="142" /></template>
17
+ <template #item-3><StatCard label="Bookings" value="38" /></template>
18
+ </DataGrid>
19
+ ```
20
+
21
+ When filling from a data array, use a dynamic slot name in a `v-for`:
22
+
23
+ ```vue
24
+ <DataGrid>
25
+ <template v-for="(item, i) in stats" #[`item-${i}`] :key="i">
26
+ <StatCard :label="item.label" :value="item.value" />
27
+ </template>
28
+ </DataGrid>
29
+ ```
30
+
31
+ ---
32
+
33
+ ## Props reference
34
+
35
+ > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
36
+
37
+ | Prop (template form) | Type | Default | Notes |
38
+ |---|---|---|---|
39
+ | `tag` | `"div" \| "section" \| "article" \| "main"` | `"div"` | Use a semantic tag for page landmark regions. |
40
+ | `:style-class-passthrough` | `string \| string[]` | `[]` | Extra CSS classes on the root element. |
41
+
42
+ ---
43
+
44
+ ## CSS custom properties
45
+
46
+ Override these via `style` attribute or a `styleClassPassthrough` class in a consuming `<style>` block.
47
+
48
+ | Property | Default | Notes |
49
+ |---|---|---|
50
+ | `--data-grid-columns` | `repeat(auto-fit, minmax(250px, 1fr))` | Full `grid-template-columns` value. Override to fix column count or change min width. |
51
+ | `--data-grid-gap` | `1rem` | Grid gap between items. |
52
+
53
+ ### Fixed column count
54
+
55
+ ```vue
56
+ <DataGrid style="--data-grid-columns: repeat(3, 1fr); --data-grid-gap: 2.4rem;">
57
+ ...
58
+ </DataGrid>
59
+ ```
60
+
61
+ ### Narrower minimum item width
62
+
63
+ ```vue
64
+ <DataGrid style="--data-grid-columns: repeat(auto-fit, minmax(180px, 1fr));">
65
+ ...
66
+ </DataGrid>
67
+ ```
68
+
69
+ ---
70
+
71
+ ## Usage examples
72
+
73
+ ### Stat cards (default auto-fit)
74
+
75
+ ```vue
76
+ <DataGrid>
77
+ <template #revenue>
78
+ <div class="stat-card">
79
+ <span class="stat-card-label">Revenue</span>
80
+ <span class="stat-card-value">£24,500</span>
81
+ </div>
82
+ </template>
83
+ <template #clients>
84
+ <div class="stat-card">
85
+ <span class="stat-card-label">Clients</span>
86
+ <span class="stat-card-value">142</span>
87
+ </div>
88
+ </template>
89
+ </DataGrid>
90
+ ```
91
+
92
+ ### Semantic section with auto aria-labelledby
93
+
94
+ ```vue
95
+ <DataGrid tag="section">
96
+ <!-- aria-labelledby is wired automatically via useAriaLabelledById -->
97
+ <template #item-1><div>Item 1</div></template>
98
+ <template #item-2><div>Item 2</div></template>
99
+ </DataGrid>
100
+ ```
101
+
102
+ ### Data-driven grid
103
+
104
+ ```vue
105
+ <script setup lang="ts">
106
+ const stats = [
107
+ { id: "revenue", label: "Revenue", value: "£24,500" },
108
+ { id: "clients", label: "Clients", value: "142" },
109
+ { id: "bookings", label: "Bookings", value: "38" },
110
+ ];
111
+ </script>
112
+
113
+ <template>
114
+ <DataGrid>
115
+ <template v-for="stat in stats" #[stat.id] :key="stat.id">
116
+ <div class="stat-card">
117
+ <span class="stat-card-label">{{ stat.label }}</span>
118
+ <span class="stat-card-value">{{ stat.value }}</span>
119
+ </div>
120
+ </template>
121
+ </DataGrid>
122
+ </template>
123
+ ```
124
+
125
+ ---
126
+
127
+ ## Accessibility
128
+
129
+ - When `tag` is `section`, `article`, or `main`, `aria-labelledby` is automatically set via `useAriaLabelledById`, pointing to a generated heading ID.
130
+ - When `tag="div"`, no ARIA attributes are added.
131
+ - Ensure a heading element with the matching ID is present inside the grid when using semantic tags.
132
+
133
+ See [component-aria-landmark.md](../component-aria-landmark.md) for the full landmark pattern.
134
+
135
+ ---
136
+
137
+ ## Local style override scaffold
138
+
139
+ ```vue
140
+ <DataGrid :style-class-passthrough="['my-data-grid']">
141
+ ...
142
+ </DataGrid>
143
+
144
+ <style>
145
+ /* ─── DataGrid local overrides ──────────────────────────────────────
146
+ Use CSS custom properties for layout, not utility classes.
147
+ Delete this block if no overrides are needed.
148
+ ─────────────────────────────────────────────────────────────────── */
149
+ .data-grid {
150
+ &.my-data-grid {
151
+ --data-grid-columns: repeat(auto-fit, minmax(200px, 1fr));
152
+ --data-grid-gap: 2rem;
153
+ }
154
+ }
155
+ </style>
156
+ ```
157
+
158
+ See [component-local-style-override.md](../component-local-style-override.md) for the full pattern.
159
+
160
+ ---
161
+
162
+ ## Notes
163
+
164
+ - Auto-imported in Nuxt — no manual import needed.
165
+ - Slot names can be anything — semantic (`#revenue`) or indexed (`#item-0`). Document order determines render order.
166
+ - `--data-grid-columns` accepts any valid `grid-template-columns` value, including named tracks and `subgrid`.
167
+ - The component does not impose a column count — `auto-fit` with `minmax` means the browser decides. Use `repeat(N, 1fr)` in `--data-grid-columns` to fix the count.
@@ -41,6 +41,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
41
41
  ├── icon-sets.md — icon set packages required by layer components, FOUC prevention, component→package map
42
42
  ├── robots-env-aware.md — @nuxtjs/robots: allow crawling on prod domain only, block on preview/staging via env var
43
43
  ├── new-app-scaffold.md — scaffold a new Nuxt consumer app extending this layer (package.json, nuxt.config, app structure, CLAUDE.md)
44
+ ├── qa-panel.md — collapsible dev-only panel for toggling component props live on a page (demo pages and consuming apps)
44
45
  ├── release-notes.md — produce release notes as a fenced markdown block from git log
45
46
  ├── composable-canonical-url.md — useCanonicalUrl: set <link rel="canonical"> from runtimeConfig.public.canonicalHost; layout setup, node types
46
47
  ├── composable-whatsapp.md — useWhatsApp: open pre-filled wa.me link from form payload; runtime config, security, usage
@@ -74,7 +75,9 @@ Each skill is a single markdown file named `<area>-<task>.md`.
74
75
  ├── social-icons-list.md — SocialIconsList: data-driven social icon links, ISocialIcon type, logos: icon names, CSS tokens
75
76
  ├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
76
77
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
77
- └── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
78
+ ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
79
+ ├── data-grid.md — DataGrid: auto-fit responsive grid, $slots iteration, --data-grid-columns/gap tokens, semantic tag + aria
80
+ └── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
78
81
  ```
79
82
 
80
83
  ## Skill file template
@@ -0,0 +1,231 @@
1
+ # QA Panel
2
+
3
+ ## Overview
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.
6
+
7
+ ## Structure
8
+
9
+ ```
10
+ <details> ← collapses the whole panel
11
+ <summary> ← always visible: title + live status line
12
+ <body> ← groups of chip buttons (and optional free-text inputs)
13
+ ```
14
+
15
+ Each group controls one prop. The status `<code>` in the summary mirrors the current state so you can see it at a glance without opening the panel.
16
+
17
+ ## Steps
18
+
19
+ ### 1. Add the reactive state to `<script setup>`
20
+
21
+ ```ts
22
+ // ── QA controls (dev only) ────────────────────────────────────────
23
+ const isDev = import.meta.dev;
24
+
25
+ // One ref per controllable prop
26
+ const qaMyBoolean = ref(true);
27
+ const qaMyNumber = ref(400);
28
+ const qaMyString = ref<"a" | "b" | "c">("a");
29
+
30
+ // Preset arrays for numeric/string chip groups
31
+ const myNumberPresets = [100, 200, 400, 800, 1600];
32
+ const myStringPresets = ["a", "b", "c"] as const;
33
+ ```
34
+
35
+ ### 2. Wire the refs to the component
36
+
37
+ ```vue
38
+ <MyComponent
39
+ :my-boolean="qaMyBoolean"
40
+ :my-number="qaMyNumber"
41
+ :my-string="qaMyString"
42
+ />
43
+ ```
44
+
45
+ ### 3. Add the panel markup
46
+
47
+ Place directly above (or below) the component being QA'd, outside any layout wrapper that clips content:
48
+
49
+ ```vue
50
+ <!-- ── QA Panel (dev only) ───────────────────────────────── -->
51
+ <div v-if="isDev" class="qa-panel">
52
+ <details class="qa-panel__details">
53
+ <summary class="qa-panel__summary">
54
+ <span class="qa-panel__title">QA — MyComponent</span>
55
+ <code class="qa-panel__status">
56
+ bool:{{ qaMyBoolean ? "on" : "off" }} · {{ qaMyNumber }}ms · {{ qaMyString }}
57
+ </code>
58
+ </summary>
59
+ <div class="qa-panel__body">
60
+
61
+ <!-- Boolean group -->
62
+ <div class="qa-panel__group">
63
+ <span class="qa-panel__label">My Boolean</span>
64
+ <div class="qa-panel__chips">
65
+ <button
66
+ v-for="opt in [true, false]"
67
+ :key="String(opt)"
68
+ class="qa-panel__chip"
69
+ :class="{ 'is-active': qaMyBoolean === opt }"
70
+ @click="qaMyBoolean = opt"
71
+ >{{ opt ? "on" : "off" }}</button>
72
+ </div>
73
+ </div>
74
+
75
+ <!-- Number group (preset chips) -->
76
+ <div class="qa-panel__group">
77
+ <span class="qa-panel__label">My Number</span>
78
+ <div class="qa-panel__chips">
79
+ <button
80
+ v-for="preset in myNumberPresets"
81
+ :key="preset"
82
+ class="qa-panel__chip"
83
+ :class="{ 'is-active': qaMyNumber === preset }"
84
+ @click="qaMyNumber = preset"
85
+ >{{ preset }}</button>
86
+ </div>
87
+ </div>
88
+
89
+ <!-- String union group -->
90
+ <div class="qa-panel__group">
91
+ <span class="qa-panel__label">My String</span>
92
+ <div class="qa-panel__chips">
93
+ <button
94
+ v-for="opt in myStringPresets"
95
+ :key="opt"
96
+ class="qa-panel__chip"
97
+ :class="{ 'is-active': qaMyString === opt }"
98
+ @click="qaMyString = opt"
99
+ >{{ opt }}</button>
100
+ </div>
101
+ </div>
102
+
103
+ <!-- Free-text input (for strings where presets aren't enough) -->
104
+ <div class="qa-panel__group">
105
+ <span class="qa-panel__label">Custom Value</span>
106
+ <input v-model="qaMyString" placeholder="e.g. 4/3" class="qa-panel__input" />
107
+ </div>
108
+
109
+ </div>
110
+ </details>
111
+ </div>
112
+ ```
113
+
114
+ ### 4. Add the CSS
115
+
116
+ Scope inside your page body class (e.g. `.my-page`) so styles don't bleed. The panel is always dark regardless of colour scheme — it's a dev tool, not a UI element.
117
+
118
+ ```css
119
+ .my-page {
120
+ /* ── QA Panel ──────────────────────────────────────────────────── */
121
+
122
+ .qa-panel {
123
+ background: oklch(15% 0 0);
124
+ color: white;
125
+ font-size: 1.3rem;
126
+ }
127
+
128
+ .qa-panel__details {
129
+ padding: 1rem 2rem;
130
+ }
131
+
132
+ .qa-panel__summary {
133
+ cursor: pointer;
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 1.6rem;
137
+ list-style: none;
138
+ user-select: none;
139
+
140
+ &::-webkit-details-marker { display: none; }
141
+ }
142
+
143
+ .qa-panel__title {
144
+ font-weight: 600;
145
+ font-size: 1.1rem;
146
+ text-transform: uppercase;
147
+ letter-spacing: 0.08em;
148
+ }
149
+
150
+ .qa-panel__status {
151
+ font-family: monospace;
152
+ font-size: 1.2rem;
153
+ background: oklch(0% 0 0 / 0.3);
154
+ padding: 0.2rem 0.8rem;
155
+ border-radius: 0.4rem;
156
+ user-select: text;
157
+ cursor: text;
158
+ }
159
+
160
+ .qa-panel__body {
161
+ display: flex;
162
+ flex-wrap: wrap;
163
+ gap: 2.4rem;
164
+ padding-block: 1.2rem 0.4rem;
165
+ }
166
+
167
+ .qa-panel__group {
168
+ display: flex;
169
+ flex-direction: column;
170
+ gap: 0.6rem;
171
+ }
172
+
173
+ .qa-panel__label {
174
+ font-size: 1.1rem;
175
+ text-transform: uppercase;
176
+ letter-spacing: 0.08em;
177
+ opacity: 0.55;
178
+ }
179
+
180
+ .qa-panel__chips {
181
+ display: flex;
182
+ flex-wrap: wrap;
183
+ gap: 0.4rem;
184
+ }
185
+
186
+ .qa-panel__chip {
187
+ font-family: monospace;
188
+ font-size: 1.2rem;
189
+ color: white;
190
+ background: oklch(0% 0 0 / 0.25);
191
+ border: 1px solid oklch(100% 0 0 / 0.18);
192
+ padding: 0.3rem 1rem;
193
+ border-radius: 0.4rem;
194
+ cursor: pointer;
195
+ transition: background 0.15s;
196
+
197
+ &:hover { background: oklch(0% 0 0 / 0.4); }
198
+
199
+ &.is-active {
200
+ background: oklch(55% 0.18 240);
201
+ border-color: oklch(55% 0.18 240);
202
+ }
203
+ }
204
+
205
+ .qa-panel__input {
206
+ font-family: monospace;
207
+ font-size: 1.2rem;
208
+ color: white;
209
+ background: oklch(0% 0 0 / 0.25);
210
+ border: 1px solid oklch(100% 0 0 / 0.18);
211
+ padding: 0.3rem 1rem;
212
+ border-radius: 0.4rem;
213
+ width: 18rem;
214
+
215
+ &::placeholder { opacity: 0.45; }
216
+ }
217
+ }
218
+ ```
219
+
220
+ ## Notes
221
+
222
+ - **Production safety**: `import.meta.dev` is `false` in production builds — the entire `v-if="isDev"` block is tree-shaken. No runtime cost.
223
+ - **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
+ - **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
+ - **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:
226
+
227
+ ```ts
228
+ const qaStyleOverrides = computed(() => ({
229
+ "--theme-component-max-height": qaMaxHeight.value || undefined,
230
+ }));
231
+ ```
@@ -0,0 +1,39 @@
1
+ <template>
2
+ <component :is="tag" class="data-grid" :class="[elementClasses]" :aria-labelledby="ariaLabelledby">
3
+ <slot v-for="(_, name) in $slots" :key="name" :name="name"></slot>
4
+ </component>
5
+ </template>
6
+
7
+ <script setup lang="ts">
8
+ interface Props {
9
+ tag?: "div" | "section" | "article" | "main";
10
+ styleClassPassthrough?: string | string[];
11
+ }
12
+
13
+ const props = withDefaults(defineProps<Props>(), {
14
+ tag: "div",
15
+ styleClassPassthrough: () => [],
16
+ });
17
+
18
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
19
+ const { ariaLabelledby } = useAriaLabelledById(props.tag);
20
+
21
+ watch(
22
+ () => props.styleClassPassthrough,
23
+ () => resetElementClasses(props.styleClassPassthrough),
24
+ );
25
+ </script>
26
+
27
+ <style lang="css">
28
+ @layer components {
29
+ .data-grid {
30
+ /* CSS Tockens for @container grid-template-columns */
31
+ --data-grid-columns: repeat(auto-fit, minmax(250px, 1fr));
32
+ --data-grid-gap: 1rem;
33
+
34
+ display: grid;
35
+ grid-template-columns: var(--data-grid-columns);
36
+ gap: var(--data-grid-gap);
37
+ }
38
+ }
39
+ </style>