srcdev-nuxt-components 9.1.42 → 9.1.43

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.
@@ -26,7 +26,9 @@
26
26
  "Bash(node -e ':*)",
27
27
  "Bash(git ls-tree *)",
28
28
  "Bash(node -p \"require\\('./package.json'\\).version\")",
29
- "Bash(git status *)"
29
+ "Bash(git status *)",
30
+ "Bash(cp /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/auto-grid.md)",
31
+ "Bash(rm /Users/simoncornforth/websites/nuxt-components/.claude/skills/components/data-grid.md)"
30
32
  ],
31
33
  "additionalDirectories": []
32
34
  }
@@ -1,8 +1,8 @@
1
- # DataGrid Component
1
+ # AutoGrid Component
2
2
 
3
3
  ## Overview
4
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.
5
+ `AutoGrid` 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
6
 
7
7
  ---
8
8
 
@@ -11,21 +11,21 @@
11
11
  Pass any number of named slots — the component renders each one in document order inside the grid.
12
12
 
13
13
  ```vue
14
- <DataGrid>
14
+ <AutoGrid>
15
15
  <template #item-1><StatCard label="Revenue" value="£24,500" /></template>
16
16
  <template #item-2><StatCard label="Clients" value="142" /></template>
17
17
  <template #item-3><StatCard label="Bookings" value="38" /></template>
18
- </DataGrid>
18
+ </AutoGrid>
19
19
  ```
20
20
 
21
21
  When filling from a data array, use a dynamic slot name in a `v-for`:
22
22
 
23
23
  ```vue
24
- <DataGrid>
24
+ <AutoGrid>
25
25
  <template v-for="(item, i) in stats" #[`item-${i}`] :key="i">
26
26
  <StatCard :label="item.label" :value="item.value" />
27
27
  </template>
28
- </DataGrid>
28
+ </AutoGrid>
29
29
  ```
30
30
 
31
31
  ---
@@ -34,10 +34,10 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
34
34
 
35
35
  > **Hyphenation rule**: Vue's ESLint config enforces `vue/attribute-hyphenation`. Always write camelCase prop names hyphenated in templates: `:style-class-passthrough`.
36
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. |
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
41
 
42
42
  ---
43
43
 
@@ -45,25 +45,27 @@ When filling from a data array, use a dynamic slot name in a `v-for`:
45
45
 
46
46
  Override these via `style` attribute or a `styleClassPassthrough` class in a consuming `<style>` block.
47
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. |
48
+ | Property | Default | Notes |
49
+ | ------------------------- | -------- | ---------------------------------------------------------------------------------------- |
50
+ | `--auto-grid-min-col-size` | `250px` | Minimum column width; browser auto-fits as many columns as will fit. |
51
+ | `--auto-grid-gap` | `1rem` | Grid gap between items. |
52
52
 
53
53
  ### Fixed column count
54
54
 
55
+ Override `grid-template-columns` directly — there is no single token for this:
56
+
55
57
  ```vue
56
- <DataGrid style="--data-grid-columns: repeat(3, 1fr); --data-grid-gap: 2.4rem;">
58
+ <AutoGrid style="grid-template-columns: repeat(3, 1fr); --auto-grid-gap: 2.4rem;">
57
59
  ...
58
- </DataGrid>
60
+ </AutoGrid>
59
61
  ```
60
62
 
61
63
  ### Narrower minimum item width
62
64
 
63
65
  ```vue
64
- <DataGrid style="--data-grid-columns: repeat(auto-fit, minmax(180px, 1fr));">
66
+ <AutoGrid style="--auto-grid-min-col-size: 180px;">
65
67
  ...
66
- </DataGrid>
68
+ </AutoGrid>
67
69
  ```
68
70
 
69
71
  ---
@@ -73,7 +75,7 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
73
75
  ### Stat cards (default auto-fit)
74
76
 
75
77
  ```vue
76
- <DataGrid>
78
+ <AutoGrid>
77
79
  <template #revenue>
78
80
  <div class="stat-card">
79
81
  <span class="stat-card-label">Revenue</span>
@@ -86,17 +88,17 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
86
88
  <span class="stat-card-value">142</span>
87
89
  </div>
88
90
  </template>
89
- </DataGrid>
91
+ </AutoGrid>
90
92
  ```
91
93
 
92
94
  ### Semantic section with auto aria-labelledby
93
95
 
94
96
  ```vue
95
- <DataGrid tag="section">
97
+ <AutoGrid tag="section">
96
98
  <!-- aria-labelledby is wired automatically via useAriaLabelledById -->
97
99
  <template #item-1><div>Item 1</div></template>
98
100
  <template #item-2><div>Item 2</div></template>
99
- </DataGrid>
101
+ </AutoGrid>
100
102
  ```
101
103
 
102
104
  ### Data-driven grid
@@ -105,20 +107,20 @@ Override these via `style` attribute or a `styleClassPassthrough` class in a con
105
107
  <script setup lang="ts">
106
108
  const stats = [
107
109
  { id: "revenue", label: "Revenue", value: "£24,500" },
108
- { id: "clients", label: "Clients", value: "142" },
110
+ { id: "clients", label: "Clients", value: "142" },
109
111
  { id: "bookings", label: "Bookings", value: "38" },
110
112
  ];
111
113
  </script>
112
114
 
113
115
  <template>
114
- <DataGrid>
116
+ <AutoGrid>
115
117
  <template v-for="stat in stats" #[stat.id] :key="stat.id">
116
118
  <div class="stat-card">
117
119
  <span class="stat-card-label">{{ stat.label }}</span>
118
120
  <span class="stat-card-value">{{ stat.value }}</span>
119
121
  </div>
120
122
  </template>
121
- </DataGrid>
123
+ </AutoGrid>
122
124
  </template>
123
125
  ```
124
126
 
@@ -137,19 +139,19 @@ See [component-aria-landmark.md](../component-aria-landmark.md) for the full lan
137
139
  ## Local style override scaffold
138
140
 
139
141
  ```vue
140
- <DataGrid :style-class-passthrough="['my-data-grid']">
142
+ <AutoGrid :style-class-passthrough="['my-auto-grid']">
141
143
  ...
142
- </DataGrid>
144
+ </AutoGrid>
143
145
 
144
146
  <style>
145
- /* ─── DataGrid local overrides ──────────────────────────────────────
147
+ /* ─── AutoGrid local overrides ──────────────────────────────────────
146
148
  Use CSS custom properties for layout, not utility classes.
147
149
  Delete this block if no overrides are needed.
148
150
  ─────────────────────────────────────────────────────────────────── */
149
- .data-grid {
150
- &.my-data-grid {
151
- --data-grid-columns: repeat(auto-fit, minmax(200px, 1fr));
152
- --data-grid-gap: 2rem;
151
+ .auto-grid {
152
+ &.my-auto-grid {
153
+ --auto-grid-min-col-size: 200px;
154
+ --auto-grid-gap: 2rem;
153
155
  }
154
156
  }
155
157
  </style>
@@ -163,5 +165,5 @@ See [component-local-style-override.md](../component-local-style-override.md) fo
163
165
 
164
166
  - Auto-imported in Nuxt — no manual import needed.
165
167
  - 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.
168
+ - `--auto-grid-min-col-size` controls the minimum column width; `auto-fit` fills as many columns as will fit.
169
+ - To fix the column count, override `grid-template-columns` directly (e.g. `style="grid-template-columns: repeat(3, 1fr)"`) there is no single token for this.
@@ -0,0 +1,87 @@
1
+ # CSS Animation Utilities
2
+
3
+ ## Overview
4
+
5
+ Scroll-driven animation utility classes bundled with the layer. Apply a class to any element to get a CSS-only, scroll-linked animation — no JavaScript required. All utilities use the [CSS Scroll-Driven Animations](https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline) spec (`animation-timeline: view()`).
6
+
7
+ **Browser support**: Chrome 115+, Firefox 110+, Safari 18+. No polyfill exists — use `@supports` for graceful degradation where needed.
8
+
9
+ All utilities are wrapped in `@media (prefers-reduced-motion: no-preference)` — animations are automatically disabled for users who prefer reduced motion.
10
+
11
+ ## Available classes
12
+
13
+ ### `.animation-scroller-x`
14
+
15
+ Scales and fades items relative to their position in a **horizontal scroll container** (carousel, horizontal list). Items at the edges are small and faint; items at the centre are full size and opaque.
16
+
17
+ ```css
18
+ animation-timeline: view(x);
19
+ ```
20
+
21
+ | Position | `opacity` | `scale` |
22
+ |---|---|---|
23
+ | Edges (0 %, 100 %) | 0.25 | 0.5 |
24
+ | Centre (35 %–65 %) | 1 | 1 |
25
+
26
+ #### Usage
27
+
28
+ ```html
29
+ <!-- Scrollable container — overflow-x must be auto or scroll -->
30
+ <div class="carousel-track" style="overflow-x: auto; display: flex;">
31
+ <div class="animation-scroller-x">Item 1</div>
32
+ <div class="animation-scroller-x">Item 2</div>
33
+ <div class="animation-scroller-x">Item 3</div>
34
+ </div>
35
+ ```
36
+
37
+ The animation is driven by the element's position inside its nearest scrollport on the x-axis. Add `.animation-scroller-x` to each **child** — not the container.
38
+
39
+ ---
40
+
41
+ ### `.entry-zoom-reveal`
42
+
43
+ Fades and zooms an element in as it scrolls into the vertical viewport (bottom 30 % inset to top 5 %).
44
+
45
+ ```html
46
+ <div class="entry-zoom-reveal">Content revealed on scroll</div>
47
+ ```
48
+
49
+ - `fill: both` — element stays hidden before entry, visible after exit
50
+
51
+ ---
52
+
53
+ ### `.entry-slide-in`
54
+
55
+ Slides an element up from 200 px below as it scrolls into view.
56
+
57
+ ```html
58
+ <div class="entry-slide-in">Slides up on scroll</div>
59
+ ```
60
+
61
+ ---
62
+
63
+ ### `.entry-exit-blur`
64
+
65
+ Blurs an element as it enters and exits the vertical viewport; sharp in the centre (45 %–55 %).
66
+
67
+ ```html
68
+ <div class="entry-exit-blur">Sharp in view, blurred at edges</div>
69
+ ```
70
+
71
+ ---
72
+
73
+ ### `.auto-rotate`
74
+
75
+ Rotates an element 0 → 360 ° as it scrolls through the vertical viewport.
76
+
77
+ ```html
78
+ <div class="auto-rotate">
79
+ <img src="/logo.svg" alt="" />
80
+ </div>
81
+ ```
82
+
83
+ ## Notes
84
+
85
+ - All classes use `animation-timeline: view()` (vertical) **except** `.animation-scroller-x` which uses `view(x)`.
86
+ - The `scroller` keyframe name used by `.animation-scroller-x` is global — avoid re-declaring `@keyframes scroller` in your own CSS.
87
+ - Utility classes are included automatically when you extend the layer — no explicit import needed in your app.
@@ -34,6 +34,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
34
34
  ├── component-local-style-override.md — styleClassPassthrough + scoped style block for per-usage visual customisation
35
35
  ├── component-prop-driven-container-layout.md — vary CSS grid layout inside @container queries using data-* attribute selectors
36
36
  ├── css-grid-max-width-gutters.md — cap a centre grid column width by growing gutters, with start/center alignment variants
37
+ ├── css-animation-utilities.md — scroll-driven animation utility classes: scroller-x (carousel), entry-zoom-reveal, entry-slide-in, entry-exit-blur, auto-rotate
37
38
  ├── component-aria-landmark.md — useAriaLabelledById composable: aria-labelledby for section/main/article/aside tags
38
39
  ├── component-export-types.md — move inline component types to app/types/components/ barrel for consumer imports
39
40
  ├── component-inline-action-button.md — InputButtonCore variant="inline" pattern for buttons embedded in custom input wrappers
@@ -76,7 +77,7 @@ Each skill is a single markdown file named `<area>-<task>.md`.
76
77
  ├── display-qr-code.md — DisplayQrCode: QR code SVG from a string value, colour/size/variant/radius props, currentColor default
77
78
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
78
79
  ├── 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
+ ├── auto-grid.md — AutoGrid: auto-fit responsive grid, $slots iteration, --auto-grid-min-col-size/gap tokens, semantic tag + aria
80
81
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
81
82
  └── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
82
83
  ```
@@ -0,0 +1,21 @@
1
+ @keyframes scroller {
2
+ 0%,
3
+ 100% {
4
+ opacity: 0.25;
5
+ scale: 0.5;
6
+ }
7
+ 35%,
8
+ 65% {
9
+ opacity: 1;
10
+ scale: 1;
11
+ }
12
+ }
13
+
14
+ /* Used to scroll scale elements when scrolling x-axis like a carousel. */
15
+
16
+ @media (prefers-reduced-motion: no-preference) {
17
+ .animation-scroller-x {
18
+ animation: scroller linear both;
19
+ animation-timeline: view(x);
20
+ }
21
+ }
@@ -7,7 +7,9 @@
7
7
  }
8
8
  }
9
9
 
10
- .auto-rotate {
11
- animation: autoRotateAnimation;
12
- animation-timeline: view();
10
+ @media (prefers-reduced-motion: no-preference) {
11
+ .auto-rotate {
12
+ animation: autoRotateAnimation;
13
+ animation-timeline: view();
14
+ }
13
15
  }
@@ -1,6 +1,8 @@
1
- .entry-exit-blur {
2
- animation: entryExitBlurAnimation linear both;
3
- animation-timeline: view();
1
+ @media (prefers-reduced-motion: no-preference) {
2
+ .entry-exit-blur {
3
+ animation: entryExitBlurAnimation linear both;
4
+ animation-timeline: view();
5
+ }
4
6
  }
5
7
  @keyframes entryExitBlurAnimation {
6
8
  0% {
@@ -9,7 +9,9 @@
9
9
  }
10
10
  }
11
11
 
12
- .entry-zoom-reveal {
13
- animation: entryZoomRevealAnimation both;
14
- animation-timeline: view(70% 5%);
12
+ @media (prefers-reduced-motion: no-preference) {
13
+ .entry-zoom-reveal {
14
+ animation: entryZoomRevealAnimation both;
15
+ animation-timeline: view(70% 5%);
16
+ }
15
17
  }
@@ -1,4 +1,5 @@
1
- @import './_entry-zoom-reveal';
2
- @import './_entry-slide-in';
3
- @import './_entry-exit-blur';
4
- @import './_auto-rotate';
1
+ @import "./_entry-zoom-reveal";
2
+ @import "./_entry-slide-in";
3
+ @import "./_entry-exit-blur";
4
+ @import "./_auto-rotate";
5
+ @import "./_animation-scroller-x.css";
@@ -0,0 +1,57 @@
1
+ <template>
2
+ <component :is="tag" class="auto-grid" :class="[elementClasses, { 'is-responsive': isResponsive }]" :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
+ isResponsive?: boolean;
11
+ styleClassPassthrough?: string | string[];
12
+ }
13
+
14
+ const props = withDefaults(defineProps<Props>(), {
15
+ tag: "div",
16
+ isResponsive: false,
17
+ styleClassPassthrough: () => [],
18
+ });
19
+
20
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
21
+ const { ariaLabelledby } = useAriaLabelledById(props.tag);
22
+
23
+ watch(
24
+ () => props.styleClassPassthrough,
25
+ () => resetElementClasses(props.styleClassPassthrough)
26
+ );
27
+ </script>
28
+
29
+ <style lang="css">
30
+ @layer components {
31
+ .auto-grid {
32
+ --auto-grid-min-col-size-small: 250px;
33
+ --auto-grid-min-col-size-default: 300px;
34
+ --auto-grid-min-col-size-large: 350px;
35
+ --auto-grid-gap: 1rem;
36
+
37
+ display: grid;
38
+ gap: var(--auto-grid-gap);
39
+
40
+ &:not(.is-responsive) {
41
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-default), 100%), 1fr));
42
+ }
43
+
44
+ &.is-responsive {
45
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-small), 100%), 1fr));
46
+
47
+ @container (width >= 768px) {
48
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-default), 100%), 1fr));
49
+ }
50
+
51
+ @container (width >= 1024px) {
52
+ grid-template-columns: repeat(auto-fit, minmax(min(var(--auto-grid-min-col-size-large), 100%), 1fr));
53
+ }
54
+ }
55
+ }
56
+ }
57
+ </style>
@@ -1,15 +1,53 @@
1
+ import { computed } from "vue";
1
2
  import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
- import DataGridComponent from "../DataGrid.vue";
3
+ import AutoGridComponent from "../AutoGrid.vue";
3
4
 
4
- const meta: Meta<typeof DataGridComponent> = {
5
- title: "Atoms/Grids/DataGrid",
6
- component: DataGridComponent,
5
+ interface AutoGridArgs {
6
+ tag: "div" | "section" | "article" | "main";
7
+ isResponsive: boolean;
8
+ styleClassPassthrough: string[];
9
+ minColSizeSmall: string;
10
+ minColSizeDefault: string;
11
+ minColSizeLarge: string;
12
+ gap: string;
13
+ }
14
+
15
+ const meta: Meta<AutoGridArgs> = {
16
+ title: "Atoms/Grids/AutoGrid",
17
+ component: AutoGridComponent,
7
18
  argTypes: {
8
19
  tag: {
9
20
  control: { type: "select" },
10
21
  options: ["div", "section", "article", "main"],
11
22
  description: "HTML tag to render as",
12
- table: { category: "Semantic" },
23
+ table: { category: "Props" },
24
+ },
25
+ isResponsive: {
26
+ control: { type: "boolean" },
27
+ description:
28
+ "Enables container-query breakpoints: `--auto-grid-min-col-size-small` below 768px, default 768px+, large 1024px+. Requires a `container-type: inline-size` ancestor.",
29
+ table: { category: "Props" },
30
+ },
31
+ minColSizeSmall: {
32
+ control: { type: "text" },
33
+ description: "`--auto-grid-min-col-size-small` — minimum column width in responsive mode below 768px",
34
+ table: { category: "CSS Tokens" },
35
+ },
36
+ minColSizeDefault: {
37
+ control: { type: "text" },
38
+ description:
39
+ "`--auto-grid-min-col-size-default` — controls column width when `isResponsive` is **off**. Also used as the mid-size step in responsive mode (768px+).",
40
+ table: { category: "CSS Tokens" },
41
+ },
42
+ minColSizeLarge: {
43
+ control: { type: "text" },
44
+ description: "`--auto-grid-min-col-size-large` — minimum column width in responsive mode 1024px+",
45
+ table: { category: "CSS Tokens" },
46
+ },
47
+ gap: {
48
+ control: { type: "text" },
49
+ description: "`--auto-grid-gap` — gap between grid items",
50
+ table: { category: "CSS Tokens" },
13
51
  },
14
52
  styleClassPassthrough: {
15
53
  table: { disable: true },
@@ -17,13 +55,18 @@ const meta: Meta<typeof DataGridComponent> = {
17
55
  },
18
56
  args: {
19
57
  tag: "div",
58
+ isResponsive: false,
59
+ minColSizeSmall: "250px",
60
+ minColSizeDefault: "300px",
61
+ minColSizeLarge: "350px",
62
+ gap: "1rem",
20
63
  styleClassPassthrough: [],
21
64
  },
22
65
  parameters: {
23
66
  docs: {
24
67
  description: {
25
68
  component:
26
- "A responsive auto-fit grid container. Uses CSS custom properties `--data-grid-columns` and `--data-grid-gap` to control layout. Renders named slots provided by the consumer.",
69
+ "A responsive auto-fit grid container. Override `--auto-grid-min-col-size-default` and `--auto-grid-gap` to control layout. Enable `isResponsive` for container-query-based column steps using `--auto-grid-min-col-size-small` and `--auto-grid-min-col-size-large`.",
27
70
  },
28
71
  },
29
72
  },
@@ -33,22 +76,24 @@ export default meta;
33
76
 
34
77
  const cardStyle =
35
78
  "padding: 2.4rem; background: white; border-radius: 0.8rem; box-shadow: 0 2px 8px rgba(0,0,0,0.08); display: flex; flex-direction: column; gap: 0.8rem;";
36
- const labelStyle = "font-size: 1.2rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280;";
79
+ const labelStyle =
80
+ "font-size: 1.2rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; color: #6b7280;";
37
81
  const valueStyle = "font-size: 2.4rem; font-weight: 700; color: #111827;";
38
82
 
39
- interface DataGridArgs {
40
- tag: "div" | "section" | "article" | "main";
41
- styleClassPassthrough: string[];
42
- }
43
-
44
- const Template: StoryFn<DataGridArgs> = (args) => ({
45
- components: { DataGridComponent },
83
+ const Template: StoryFn<AutoGridArgs> = (args) => ({
84
+ components: { AutoGridComponent },
46
85
  setup() {
47
- return { args };
86
+ const cssTokenStyle = computed(() => ({
87
+ "--auto-grid-min-col-size-small": args.minColSizeSmall,
88
+ "--auto-grid-min-col-size-default": args.minColSizeDefault,
89
+ "--auto-grid-min-col-size-large": args.minColSizeLarge,
90
+ "--auto-grid-gap": args.gap,
91
+ }));
92
+ return { args, cssTokenStyle };
48
93
  },
49
94
  template: `
50
- <div style="padding: 3.2rem; background: #f9fafb;">
51
- <DataGridComponent :tag="args.tag" :style-class-passthrough="args.styleClassPassthrough">
95
+ <div style="padding: 3.2rem; background: #f9fafb; container-type: inline-size;">
96
+ <AutoGridComponent :tag="args.tag" :is-responsive="args.isResponsive" :style="cssTokenStyle" :style-class-passthrough="args.styleClassPassthrough">
52
97
  <template #item-1>
53
98
  <div style="${cardStyle}">
54
99
  <span style="${labelStyle}">Revenue</span>
@@ -73,7 +118,7 @@ const Template: StoryFn<DataGridArgs> = (args) => ({
73
118
  <span style="${valueStyle}">4.9</span>
74
119
  </div>
75
120
  </template>
76
- </DataGridComponent>
121
+ </AutoGridComponent>
77
122
  </div>
78
123
  `,
79
124
  });
@@ -82,7 +127,18 @@ export const Default = Template.bind({});
82
127
  Default.parameters = {
83
128
  docs: {
84
129
  description: {
85
- story: "Default grid with four stat cards. Columns auto-fit to available space at a minimum of 250px each.",
130
+ story: "Default grid with four stat cards. Columns auto-fit to available space at a minimum of 300px each.",
131
+ },
132
+ },
133
+ };
134
+
135
+ export const Responsive = Template.bind({});
136
+ Responsive.args = { isResponsive: true };
137
+ Responsive.parameters = {
138
+ docs: {
139
+ description: {
140
+ story:
141
+ "With `isResponsive` enabled, column width steps through three sizes based on container width. Resize the story viewport to see the breakpoints in action.",
86
142
  },
87
143
  },
88
144
  };
@@ -91,10 +147,10 @@ export const TwoItems = Template.bind({});
91
147
  TwoItems.storyName = "Two Items";
92
148
  TwoItems.decorators = [
93
149
  () => ({
94
- components: { DataGridComponent },
150
+ components: { AutoGridComponent },
95
151
  template: `
96
- <div style="padding: 3.2rem; background: #f9fafb;">
97
- <DataGridComponent>
152
+ <div style="padding: 3.2rem; background: #f9fafb; container-type: inline-size;">
153
+ <AutoGridComponent>
98
154
  <template #item-1>
99
155
  <div style="${cardStyle}">
100
156
  <span style="${labelStyle}">Revenue</span>
@@ -107,7 +163,7 @@ TwoItems.decorators = [
107
163
  <span style="${valueStyle}">142</span>
108
164
  </div>
109
165
  </template>
110
- </DataGridComponent>
166
+ </AutoGridComponent>
111
167
  </div>
112
168
  `,
113
169
  }),
@@ -116,15 +172,15 @@ TwoItems.parameters = {
116
172
  docs: { description: { story: "Grid with two items — auto-fit spreads them to fill available columns." } },
117
173
  };
118
174
 
119
- export const NarrowContainer: StoryFn<DataGridArgs> = (args) => ({
120
- components: { DataGridComponent },
175
+ export const NarrowContainer: StoryFn<AutoGridArgs> = (args) => ({
176
+ components: { AutoGridComponent },
121
177
  setup() {
122
178
  return { args };
123
179
  },
124
180
  template: `
125
181
  <div style="padding: 3.2rem; background: #f9fafb;">
126
- <div style="max-width: 400px;">
127
- <DataGridComponent :tag="args.tag">
182
+ <div style="max-width: 400px; container-type: inline-size;">
183
+ <AutoGridComponent :tag="args.tag">
128
184
  <template #item-1>
129
185
  <div style="${cardStyle}">
130
186
  <span style="${labelStyle}">Revenue</span>
@@ -143,7 +199,7 @@ export const NarrowContainer: StoryFn<DataGridArgs> = (args) => ({
143
199
  <span style="${valueStyle}">38</span>
144
200
  </div>
145
201
  </template>
146
- </DataGridComponent>
202
+ </AutoGridComponent>
147
203
  </div>
148
204
  </div>
149
205
  `,
@@ -151,19 +207,19 @@ export const NarrowContainer: StoryFn<DataGridArgs> = (args) => ({
151
207
  NarrowContainer.parameters = {
152
208
  docs: {
153
209
  description: {
154
- story: "In a 400px container the auto-fit columns stack to a single column once items fall below 250px.",
210
+ story: "In a 400px container the auto-fit columns stack to a single column once items fall below 300px.",
155
211
  },
156
212
  },
157
213
  };
158
214
 
159
- export const CustomColumns: StoryFn<DataGridArgs> = (args) => ({
160
- components: { DataGridComponent },
215
+ export const CustomColumns: StoryFn<AutoGridArgs> = (args) => ({
216
+ components: { AutoGridComponent },
161
217
  setup() {
162
218
  return { args };
163
219
  },
164
220
  template: `
165
221
  <div style="padding: 3.2rem; background: #f9fafb;">
166
- <DataGridComponent style="--data-grid-columns: repeat(3, 1fr); --data-grid-gap: 2.4rem;" :tag="args.tag">
222
+ <AutoGridComponent style="grid-template-columns: repeat(3, 1fr); --auto-grid-gap: 2.4rem;" :tag="args.tag">
167
223
  <template #item-1>
168
224
  <div style="${cardStyle}">
169
225
  <span style="${labelStyle}">Revenue</span>
@@ -182,7 +238,7 @@ export const CustomColumns: StoryFn<DataGridArgs> = (args) => ({
182
238
  <span style="${valueStyle}">38</span>
183
239
  </div>
184
240
  </template>
185
- </DataGridComponent>
241
+ </AutoGridComponent>
186
242
  </div>
187
243
  `,
188
244
  });
@@ -190,19 +246,19 @@ CustomColumns.parameters = {
190
246
  docs: {
191
247
  description: {
192
248
  story:
193
- "Override `--data-grid-columns` and `--data-grid-gap` via inline style to force a fixed 3-column layout with wider gaps.",
249
+ "Override `grid-template-columns` and `--auto-grid-gap` via inline style to force a fixed 3-column layout with wider gaps.",
194
250
  },
195
251
  },
196
252
  };
197
253
 
198
- export const SemanticSection: StoryFn<DataGridArgs> = (args) => ({
199
- components: { DataGridComponent },
254
+ export const SemanticSection: StoryFn<AutoGridArgs> = (args) => ({
255
+ components: { AutoGridComponent },
200
256
  setup() {
201
257
  return { args };
202
258
  },
203
259
  template: `
204
- <div style="padding: 3.2rem; background: #f9fafb;">
205
- <DataGridComponent tag="section">
260
+ <div style="padding: 3.2rem; background: #f9fafb; container-type: inline-size;">
261
+ <AutoGridComponent tag="section">
206
262
  <template #item-1>
207
263
  <div style="${cardStyle}">
208
264
  <span style="${labelStyle}">Revenue</span>
@@ -221,7 +277,7 @@ export const SemanticSection: StoryFn<DataGridArgs> = (args) => ({
221
277
  <span style="${valueStyle}">38</span>
222
278
  </div>
223
279
  </template>
224
- </DataGridComponent>
280
+ </AutoGridComponent>
225
281
  </div>
226
282
  `,
227
283
  });
@@ -1,26 +1,27 @@
1
1
  import { describe, it, expect } from "vitest";
2
2
  import { mountSuspended } from "@nuxt/test-utils/runtime";
3
- import DataGrid from "../DataGrid.vue";
3
+ import AutoGrid from "../AutoGrid.vue";
4
4
 
5
- describe("DataGrid", () => {
5
+ describe("AutoGrid", () => {
6
6
  // ─── Mount ───────────────────────────────────────────────────────────────
7
7
 
8
8
  it("mounts without error", async () => {
9
- const wrapper = await mountSuspended(DataGrid);
9
+ const wrapper = await mountSuspended(AutoGrid);
10
10
  expect(wrapper.vm).toBeTruthy();
11
11
  });
12
12
 
13
13
  // ─── Snapshots ───────────────────────────────────────────────────────────
14
14
 
15
15
  it("renders correct HTML structure (default props)", async () => {
16
- const wrapper = await mountSuspended(DataGrid);
16
+ const wrapper = await mountSuspended(AutoGrid);
17
17
  expect(wrapper.html()).toMatchSnapshot();
18
18
  });
19
19
 
20
20
  it("renders correct HTML structure (all props and slots set)", async () => {
21
- const wrapper = await mountSuspended(DataGrid, {
21
+ const wrapper = await mountSuspended(AutoGrid, {
22
22
  props: {
23
23
  tag: "section",
24
+ isResponsive: true,
24
25
  styleClassPassthrough: ["custom-class"],
25
26
  },
26
27
  slots: {
@@ -35,58 +36,77 @@ describe("DataGrid", () => {
35
36
  // ─── Root element ────────────────────────────────────────────────────────
36
37
 
37
38
  it("renders a <div> as the root element by default", async () => {
38
- const wrapper = await mountSuspended(DataGrid);
39
+ const wrapper = await mountSuspended(AutoGrid);
39
40
  expect(wrapper.element.tagName).toBe("DIV");
40
41
  });
41
42
 
42
- it("always has the data-grid class", async () => {
43
- const wrapper = await mountSuspended(DataGrid);
44
- expect(wrapper.classes()).toContain("data-grid");
43
+ it("always has the auto-grid class", async () => {
44
+ const wrapper = await mountSuspended(AutoGrid);
45
+ expect(wrapper.classes()).toContain("auto-grid");
45
46
  });
46
47
 
47
48
  // ─── Tag prop ────────────────────────────────────────────────────────────
48
49
 
49
50
  it("renders a <section> when tag is section", async () => {
50
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "section" } });
51
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "section" } });
51
52
  expect(wrapper.element.tagName).toBe("SECTION");
52
53
  });
53
54
 
54
55
  it("renders an <article> when tag is article", async () => {
55
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "article" } });
56
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "article" } });
56
57
  expect(wrapper.element.tagName).toBe("ARTICLE");
57
58
  });
58
59
 
59
60
  it("renders a <main> when tag is main", async () => {
60
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "main" } });
61
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "main" } });
61
62
  expect(wrapper.element.tagName).toBe("MAIN");
62
63
  });
63
64
 
64
65
  // ─── Aria ─────────────────────────────────────────────────────────────────
65
66
 
66
67
  it("does not set aria-labelledby when tag is div", async () => {
67
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "div" } });
68
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "div" } });
68
69
  expect(wrapper.attributes("aria-labelledby")).toBeUndefined();
69
70
  });
70
71
 
71
72
  it("sets aria-labelledby when tag is section", async () => {
72
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "section" } });
73
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "section" } });
73
74
  expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
74
75
  });
75
76
 
76
77
  it("sets aria-labelledby when tag is article", async () => {
77
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "article" } });
78
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "article" } });
78
79
  expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
79
80
  });
80
81
 
81
82
  it("sets aria-labelledby when tag is main", async () => {
82
- const wrapper = await mountSuspended(DataGrid, { props: { tag: "main" } });
83
+ const wrapper = await mountSuspended(AutoGrid, { props: { tag: "main" } });
83
84
  expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
84
85
  });
85
86
 
87
+ // ─── isResponsive ────────────────────────────────────────────────────────
88
+
89
+ it("does not have is-responsive class by default", async () => {
90
+ const wrapper = await mountSuspended(AutoGrid);
91
+ expect(wrapper.classes()).not.toContain("is-responsive");
92
+ });
93
+
94
+ it("adds is-responsive class when isResponsive is true", async () => {
95
+ const wrapper = await mountSuspended(AutoGrid, { props: { isResponsive: true } });
96
+ expect(wrapper.classes()).toContain("is-responsive");
97
+ });
98
+
99
+ it("removes is-responsive class when isResponsive changes to false", async () => {
100
+ const wrapper = await mountSuspended(AutoGrid, { props: { isResponsive: true } });
101
+ expect(wrapper.classes()).toContain("is-responsive");
102
+ await wrapper.setProps({ isResponsive: false });
103
+ expect(wrapper.classes()).not.toContain("is-responsive");
104
+ });
105
+
86
106
  // ─── Slots ───────────────────────────────────────────────────────────────
87
107
 
88
108
  it("renders a named slot", async () => {
89
- const wrapper = await mountSuspended(DataGrid, {
109
+ const wrapper = await mountSuspended(AutoGrid, {
90
110
  slots: { "item-1": "<div class='card'>Card 1</div>" },
91
111
  });
92
112
  expect(wrapper.find(".card").exists()).toBe(true);
@@ -94,7 +114,7 @@ describe("DataGrid", () => {
94
114
  });
95
115
 
96
116
  it("renders multiple named slots", async () => {
97
- const wrapper = await mountSuspended(DataGrid, {
117
+ const wrapper = await mountSuspended(AutoGrid, {
98
118
  slots: {
99
119
  "item-1": "<div class='card-1'>Card 1</div>",
100
120
  "item-2": "<div class='card-2'>Card 2</div>",
@@ -107,21 +127,21 @@ describe("DataGrid", () => {
107
127
  });
108
128
 
109
129
  it("renders no slot content when no slots are provided", async () => {
110
- const wrapper = await mountSuspended(DataGrid);
130
+ const wrapper = await mountSuspended(AutoGrid);
111
131
  expect(wrapper.text()).toBe("");
112
132
  });
113
133
 
114
134
  // ─── styleClassPassthrough ───────────────────────────────────────────────
115
135
 
116
136
  it("applies a single styleClassPassthrough string to the root", async () => {
117
- const wrapper = await mountSuspended(DataGrid, {
137
+ const wrapper = await mountSuspended(AutoGrid, {
118
138
  props: { styleClassPassthrough: "my-grid" },
119
139
  });
120
140
  expect(wrapper.classes()).toContain("my-grid");
121
141
  });
122
142
 
123
143
  it("applies multiple styleClassPassthrough classes from an array", async () => {
124
- const wrapper = await mountSuspended(DataGrid, {
144
+ const wrapper = await mountSuspended(AutoGrid, {
125
145
  props: { styleClassPassthrough: ["my-grid", "mbe-32"] },
126
146
  });
127
147
  expect(wrapper.classes()).toContain("my-grid");
@@ -129,7 +149,7 @@ describe("DataGrid", () => {
129
149
  });
130
150
 
131
151
  it("updates classes when styleClassPassthrough prop changes", async () => {
132
- const wrapper = await mountSuspended(DataGrid, {
152
+ const wrapper = await mountSuspended(AutoGrid, {
133
153
  props: { styleClassPassthrough: ["original"] },
134
154
  });
135
155
  expect(wrapper.classes()).toContain("original");
@@ -0,0 +1,11 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`AutoGrid > renders correct HTML structure (all props and slots set) 1`] = `
4
+ "<section class="auto-grid custom-class is-responsive" aria-labelledby="v-0-0">
5
+ <div>Item 1</div>
6
+ <div>Item 2</div>
7
+ <div>Item 3</div>
8
+ </section>"
9
+ `;
10
+
11
+ exports[`AutoGrid > renders correct HTML structure (default props) 1`] = `"<div class="auto-grid"></div>"`;
@@ -1,11 +1,11 @@
1
1
  // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
2
 
3
- exports[`DataGrid > renders correct HTML structure (all props and slots set) 1`] = `
4
- "<section class="data-grid custom-class" aria-labelledby="v-0-0">
3
+ exports[`AutoGrid > renders correct HTML structure (all props and slots set) 1`] = `
4
+ "<section class="auto-grid custom-class" aria-labelledby="v-0-0">
5
5
  <div>Item 1</div>
6
6
  <div>Item 2</div>
7
7
  <div>Item 3</div>
8
8
  </section>"
9
9
  `;
10
10
 
11
- exports[`DataGrid > renders correct HTML structure (default props) 1`] = `"<div class="data-grid"></div>"`;
11
+ exports[`AutoGrid > renders correct HTML structure (default props) 1`] = `"<div class="auto-grid"></div>"`;
@@ -0,0 +1,311 @@
1
+ <template>
2
+ <div>
3
+ <NuxtLayout name="default">
4
+ <template #layout-content>
5
+
6
+ <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-32']">
7
+ <h1 class="page-heading-1">AutoGrid</h1>
8
+ <p class="page-body-normal">
9
+ Responsive auto-fit CSS grid wrapper. Columns auto-fit to a minimum width; when
10
+ <code>is-responsive</code> is enabled, the minimum shifts at container breakpoints via
11
+ <code>@container</code> queries.
12
+ </p>
13
+ </LayoutRow>
14
+
15
+ <div v-if="isDev" class="qa-panel">
16
+ <details class="qa-panel__details">
17
+ <summary class="qa-panel__summary">
18
+ <span class="qa-panel__title">QA — AutoGrid</span>
19
+ <code class="qa-panel__status">
20
+ tag:{{ qaTag }} · responsive:{{ qaIsResponsive ? "on" : "off" }} · small:{{ qaMinColSizeSmall }} · default:{{ qaMinColSizeDefault }} · large:{{ qaMinColSizeLarge }} · gap:{{ qaGap }}
21
+ </code>
22
+ </summary>
23
+ <div class="qa-panel__body">
24
+
25
+ <div class="qa-panel__group">
26
+ <span class="qa-panel__label">tag</span>
27
+ <div class="qa-panel__chips">
28
+ <button
29
+ v-for="opt in tagOptions"
30
+ :key="opt"
31
+ class="qa-panel__chip"
32
+ :class="{ 'is-active': qaTag === opt }"
33
+ @click="qaTag = opt"
34
+ >{{ opt }}</button>
35
+ </div>
36
+ </div>
37
+
38
+ <div class="qa-panel__group">
39
+ <span class="qa-panel__label">is-responsive</span>
40
+ <div class="qa-panel__chips">
41
+ <button
42
+ v-for="opt in [true, false]"
43
+ :key="String(opt)"
44
+ class="qa-panel__chip"
45
+ :class="{ 'is-active': qaIsResponsive === opt }"
46
+ @click="qaIsResponsive = opt"
47
+ >{{ opt ? "on" : "off" }}</button>
48
+ </div>
49
+ </div>
50
+
51
+ <div class="qa-panel__group">
52
+ <span class="qa-panel__label">min-col-size-small</span>
53
+ <div class="qa-panel__chips">
54
+ <button
55
+ v-for="preset in sizePresets"
56
+ :key="preset"
57
+ class="qa-panel__chip"
58
+ :class="{ 'is-active': qaMinColSizeSmall === preset }"
59
+ @click="qaMinColSizeSmall = preset"
60
+ >{{ preset }}</button>
61
+ </div>
62
+ <input v-model="qaMinColSizeSmall" placeholder="e.g. 200px" class="qa-panel__input" />
63
+ </div>
64
+
65
+ <div class="qa-panel__group">
66
+ <span class="qa-panel__label">min-col-size-default</span>
67
+ <div class="qa-panel__chips">
68
+ <button
69
+ v-for="preset in sizePresets"
70
+ :key="preset"
71
+ class="qa-panel__chip"
72
+ :class="{ 'is-active': qaMinColSizeDefault === preset }"
73
+ @click="qaMinColSizeDefault = preset"
74
+ >{{ preset }}</button>
75
+ </div>
76
+ <input v-model="qaMinColSizeDefault" placeholder="e.g. 300px" class="qa-panel__input" />
77
+ </div>
78
+
79
+ <div class="qa-panel__group">
80
+ <span class="qa-panel__label">min-col-size-large</span>
81
+ <div class="qa-panel__chips">
82
+ <button
83
+ v-for="preset in sizePresets"
84
+ :key="preset"
85
+ class="qa-panel__chip"
86
+ :class="{ 'is-active': qaMinColSizeLarge === preset }"
87
+ @click="qaMinColSizeLarge = preset"
88
+ >{{ preset }}</button>
89
+ </div>
90
+ <input v-model="qaMinColSizeLarge" placeholder="e.g. 350px" class="qa-panel__input" />
91
+ </div>
92
+
93
+ <div class="qa-panel__group">
94
+ <span class="qa-panel__label">gap</span>
95
+ <div class="qa-panel__chips">
96
+ <button
97
+ v-for="preset in gapPresets"
98
+ :key="preset"
99
+ class="qa-panel__chip"
100
+ :class="{ 'is-active': qaGap === preset }"
101
+ @click="qaGap = preset"
102
+ >{{ preset }}</button>
103
+ </div>
104
+ <input v-model="qaGap" placeholder="e.g. 2rem" class="qa-panel__input" />
105
+ </div>
106
+
107
+ </div>
108
+ </details>
109
+ </div>
110
+
111
+ <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-32']">
112
+ <div class="auto-grid-demo-container">
113
+ <AutoGrid
114
+ :tag="qaTag"
115
+ :is-responsive="qaIsResponsive"
116
+ :min-col-size-small="qaMinColSizeSmall"
117
+ :min-col-size-default="qaMinColSizeDefault"
118
+ :min-col-size-large="qaMinColSizeLarge"
119
+ :style="qaStyle"
120
+ >
121
+ <template v-for="card in statCards" #[card.id] :key="card.id">
122
+ <div class="stat-card">
123
+ <span class="stat-card__label">{{ card.label }}</span>
124
+ <span class="stat-card__value">{{ card.value }}</span>
125
+ </div>
126
+ </template>
127
+ </AutoGrid>
128
+ </div>
129
+ </LayoutRow>
130
+
131
+ </template>
132
+ </NuxtLayout>
133
+ </div>
134
+ </template>
135
+
136
+ <script setup lang="ts">
137
+ definePageMeta({ layout: false });
138
+
139
+ useHead({
140
+ title: "AutoGrid",
141
+ meta: [{ name: "description", content: "AutoGrid component demo" }],
142
+ bodyAttrs: { class: "auto-grid-page" },
143
+ });
144
+
145
+ const isDev = import.meta.dev;
146
+
147
+ const tagOptions = ["div", "section", "article", "main"] as const;
148
+ const qaTag = ref<"div" | "section" | "article" | "main">("div");
149
+ const qaIsResponsive = ref(false);
150
+ const qaMinColSizeSmall = ref("250px");
151
+ const qaMinColSizeDefault = ref("300px");
152
+ const qaMinColSizeLarge = ref("350px");
153
+ const qaGap = ref("1rem");
154
+
155
+ const sizePresets = ["150px", "200px", "250px", "300px", "350px", "400px"];
156
+ const gapPresets = ["0.5rem", "1rem", "1.6rem", "2.4rem", "3.2rem"];
157
+
158
+ const qaStyle = computed(() => ({
159
+ "--auto-grid-min-col-size-small": qaMinColSizeSmall.value,
160
+ "--auto-grid-min-col-size-default": qaMinColSizeDefault.value,
161
+ "--auto-grid-min-col-size-large": qaMinColSizeLarge.value,
162
+ "--auto-grid-gap": qaGap.value,
163
+ }));
164
+
165
+ const statCards = [
166
+ { id: "item-1", label: "Revenue", value: "£24,500" },
167
+ { id: "item-2", label: "Clients", value: "142" },
168
+ { id: "item-3", label: "Bookings", value: "38" },
169
+ { id: "item-4", label: "Avg. Rating", value: "4.9" },
170
+ { id: "item-5", label: "New Users", value: "76" },
171
+ { id: "item-6", label: "Retention", value: "91%" },
172
+ ];
173
+ </script>
174
+
175
+ <style lang="css">
176
+ .auto-grid-page {
177
+ code {
178
+ font-family: monospace;
179
+ font-size: 0.9em;
180
+ background: var(--slate-02);
181
+ padding: 0.1em 0.4em;
182
+ border-radius: 0.3rem;
183
+ }
184
+
185
+ .auto-grid-demo-container {
186
+ container-type: inline-size;
187
+ }
188
+
189
+ .stat-card {
190
+ display: flex;
191
+ flex-direction: column;
192
+ gap: 0.8rem;
193
+ padding: 2.4rem;
194
+ background: white;
195
+ border-radius: 0.8rem;
196
+ box-shadow: 0 2px 8px oklch(0% 0 0 / 0.08);
197
+ }
198
+
199
+ .stat-card__label {
200
+ font-size: 1.2rem;
201
+ font-weight: 600;
202
+ text-transform: uppercase;
203
+ letter-spacing: 0.05em;
204
+ color: var(--slate-08, #6b7280);
205
+ }
206
+
207
+ .stat-card__value {
208
+ font-size: 2.4rem;
209
+ font-weight: 700;
210
+ color: var(--slate-12, #111827);
211
+ }
212
+
213
+ /* ── QA Panel ──────────────────────────────────────────────────── */
214
+
215
+ .qa-panel {
216
+ background: oklch(15% 0 0);
217
+ color: white;
218
+ font-size: 1.3rem;
219
+ }
220
+
221
+ .qa-panel__details {
222
+ padding: 1rem 2rem;
223
+ }
224
+
225
+ .qa-panel__summary {
226
+ cursor: pointer;
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 1.6rem;
230
+ list-style: none;
231
+ user-select: none;
232
+
233
+ &::-webkit-details-marker { display: none; }
234
+ }
235
+
236
+ .qa-panel__title {
237
+ font-weight: 600;
238
+ font-size: 1.1rem;
239
+ text-transform: uppercase;
240
+ letter-spacing: 0.08em;
241
+ }
242
+
243
+ .qa-panel__status {
244
+ font-family: monospace;
245
+ font-size: 1.2rem;
246
+ background: oklch(0% 0 0 / 0.3);
247
+ padding: 0.2rem 0.8rem;
248
+ border-radius: 0.4rem;
249
+ user-select: text;
250
+ cursor: text;
251
+ }
252
+
253
+ .qa-panel__body {
254
+ display: flex;
255
+ flex-wrap: wrap;
256
+ gap: 2.4rem;
257
+ padding-block: 1.2rem 0.4rem;
258
+ }
259
+
260
+ .qa-panel__group {
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 0.6rem;
264
+ }
265
+
266
+ .qa-panel__label {
267
+ font-size: 1.1rem;
268
+ text-transform: uppercase;
269
+ letter-spacing: 0.08em;
270
+ opacity: 0.55;
271
+ }
272
+
273
+ .qa-panel__chips {
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ gap: 0.4rem;
277
+ }
278
+
279
+ .qa-panel__chip {
280
+ font-family: monospace;
281
+ font-size: 1.2rem;
282
+ color: white;
283
+ background: oklch(0% 0 0 / 0.25);
284
+ border: 1px solid oklch(100% 0 0 / 0.18);
285
+ padding: 0.3rem 1rem;
286
+ border-radius: 0.4rem;
287
+ cursor: pointer;
288
+ transition: background 0.15s;
289
+
290
+ &:hover { background: oklch(0% 0 0 / 0.4); }
291
+
292
+ &.is-active {
293
+ background: oklch(55% 0.18 240);
294
+ border-color: oklch(55% 0.18 240);
295
+ }
296
+ }
297
+
298
+ .qa-panel__input {
299
+ font-family: monospace;
300
+ font-size: 1.2rem;
301
+ color: white;
302
+ background: oklch(0% 0 0 / 0.25);
303
+ border: 1px solid oklch(100% 0 0 / 0.18);
304
+ padding: 0.3rem 1rem;
305
+ border-radius: 0.4rem;
306
+ width: 18rem;
307
+
308
+ &::placeholder { opacity: 0.45; }
309
+ }
310
+ }
311
+ </style>
@@ -7,6 +7,11 @@
7
7
  <NuxtLink class="page-body-normal" to="/grid-stack">GridStack</NuxtLink>
8
8
  </LayoutRow>
9
9
 
10
+ <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
11
+ <h2 class="page-heading-2">AutoGrid</h2>
12
+ <NuxtLink class="page-body-normal" to="/auto-grid">AutoGrid</NuxtLink>
13
+ </LayoutRow>
14
+
10
15
  <LayoutRow tag="div" variant="full-width" :style-class-passthrough="['mbe-20']">
11
16
  <h1 class="page-heading-1">Nuxt3 Components Layer</h1>
12
17
  <p>
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.42",
4
+ "version": "9.1.43",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",
@@ -1,39 +0,0 @@
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>