srcdev-nuxt-components 9.1.42 → 9.1.44

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 (31) hide show
  1. package/.claude/settings.json +3 -1
  2. package/.claude/skills/components/{data-grid.md → auto-grid.md} +36 -34
  3. package/.claude/skills/components/display-avatar.md +187 -0
  4. package/.claude/skills/components/display-chip.md +213 -0
  5. package/.claude/skills/css-animation-utilities.md +87 -0
  6. package/.claude/skills/index.md +4 -1
  7. package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +21 -0
  8. package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
  9. package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
  10. package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
  11. package/app/assets/styles/setup/06.utility-classes/animations/index.css +5 -4
  12. package/app/components/01.atoms/display-avatar/DisplayAvatar.vue +130 -0
  13. package/app/components/{display-avatar → 01.atoms/display-avatar}/stories/DisplayAvatar.stories.ts +12 -0
  14. package/app/components/01.atoms/display-avatar/tests/DisplayAvatar.spec.ts +208 -0
  15. package/app/components/01.atoms/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +11 -0
  16. package/app/components/01.atoms/grids/data-grid/AutoGrid.vue +57 -0
  17. package/app/components/01.atoms/grids/data-grid/stories/{DataGrid.stories.ts → AutoGrid.stories.ts} +95 -39
  18. package/app/components/01.atoms/grids/data-grid/tests/{DataGrid.spec.ts → AutoGrid.spec.ts} +42 -22
  19. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +11 -0
  20. package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +3 -3
  21. package/app/components/02.molecules/display-chip/DisplayChip.vue +189 -0
  22. package/app/components/{display-chip → 02.molecules/display-chip}/stories/DisplayChip.stories.ts +37 -53
  23. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +191 -0
  24. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +12 -0
  25. package/app/pages/auto-grid.vue +311 -0
  26. package/app/pages/index.vue +5 -0
  27. package/app/pages/ui/display-chip.vue +201 -66
  28. package/package.json +1 -1
  29. package/app/components/01.atoms/grids/data-grid/DataGrid.vue +0 -39
  30. package/app/components/display-avatar/DisplayAvatar.vue +0 -148
  31. package/app/components/display-chip/DisplayChip.vue +0 -187
@@ -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,9 @@ 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
81
+ ├── display-avatar.md — DisplayAvatar: circular avatar with image/initials fallback, size variants, chip badge, icon slot, styleClassPassthrough
82
+ ├── display-chip.md — DisplayChip: status indicator chip overlay, CSS trig positioning, circle/square shapes, status colours, icon/label content
80
83
  ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
81
84
  └── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
82
85
  ```
@@ -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,130 @@
1
+ <template>
2
+ <component
3
+ :is="props.chip ? DisplayChip : as"
4
+ v-bind="
5
+ props.chip
6
+ ? typeof props.chip === 'object'
7
+ ? { tag: chipTag, config: props.chip }
8
+ : { tag: chipTag, config: chipDefaultConfig }
9
+ : {}
10
+ "
11
+ class="display-avatar"
12
+ :class="[size, elementClasses]"
13
+ :style-class-passthrough="elementClasses"
14
+ >
15
+ <slot name="default">
16
+ <NuxtImg v-if="src" :src :alt="alt || 'Avatar'" width="100%" height="100%" class="avatar-image" />
17
+ <span v-else>{{ fallback }}</span>
18
+ </slot>
19
+ <slot name="icon"></slot>
20
+ </component>
21
+ </template>
22
+
23
+ <script setup lang="ts">
24
+ import DisplayChip from "../../02.molecules/display-chip/DisplayChip.vue";
25
+ import type { DisplayChipConfig } from "~/types/components";
26
+
27
+ interface Props {
28
+ as?: string | object;
29
+ src?: string;
30
+ alt?: string;
31
+ text?: string;
32
+ size?: "xs" | "s" | "md" | "lg" | "xl" | string;
33
+ chip?: boolean | DisplayChipConfig;
34
+ styleClassPassthrough?: string | string[];
35
+ }
36
+
37
+ const props = withDefaults(defineProps<Props>(), {
38
+ as: "span",
39
+ src: undefined,
40
+ alt: undefined,
41
+ text: undefined,
42
+ size: "md",
43
+ chip: undefined,
44
+ styleClassPassthrough: () => [],
45
+ });
46
+
47
+ const { elementClasses, resetElementClasses, updateElementClasses } = useStyleClassPassthrough(
48
+ props.styleClassPassthrough
49
+ );
50
+
51
+ if (props.chip && typeof props.chip === "object" && !("styleClassPassthrough" in props.chip)) {
52
+ updateElementClasses(["display-avatar", props.size]);
53
+ }
54
+
55
+ const fallback = computed(
56
+ () =>
57
+ props.text ||
58
+ (props.alt || "")
59
+ .split(" ")
60
+ .map((word) => word.charAt(0))
61
+ .join("")
62
+ .substring(0, 2)
63
+ );
64
+
65
+ const chipDefaultConfig: DisplayChipConfig = {
66
+ size: "12px",
67
+ maskWidth: "4px",
68
+ offset: "0px",
69
+ angle: "90deg",
70
+ };
71
+
72
+ const chipTag = computed((): "div" | "span" => (props.as === "div" || props.as === "span" ? props.as : "span"));
73
+
74
+ watch(
75
+ () => props.styleClassPassthrough,
76
+ () => {
77
+ resetElementClasses(props.styleClassPassthrough);
78
+ }
79
+ );
80
+ </script>
81
+
82
+ <style lang="css">
83
+ @layer components {
84
+ .display-avatar {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ border-radius: 50%;
89
+ color: var(--slate-03);
90
+
91
+ isolation: isolate;
92
+
93
+ &.xs {
94
+ width: 24px;
95
+ height: 24px;
96
+ font-size: 0.75rem;
97
+ }
98
+ &.s {
99
+ width: 32px;
100
+ height: 32px;
101
+ font-size: 0.875rem;
102
+ }
103
+ &.md {
104
+ width: 40px;
105
+ height: 40px;
106
+ font-size: 1rem;
107
+ }
108
+ &.lg {
109
+ width: 48px;
110
+ height: 48px;
111
+ font-size: 1.125rem;
112
+ }
113
+ &.xl {
114
+ width: 56px;
115
+ height: 56px;
116
+ font-size: 1.25rem;
117
+ }
118
+
119
+ .avatar-image {
120
+ width: 100%;
121
+ border-radius: 50%;
122
+ object-fit: cover;
123
+ }
124
+
125
+ .avatar-icon {
126
+ font-size: 24px;
127
+ }
128
+ }
129
+ }
130
+ </style>
@@ -5,6 +5,14 @@ export default {
5
5
  title: "Components/UI/DisplayAvatar",
6
6
  component: StorybookComponent,
7
7
  argTypes: {
8
+ size: {
9
+ control: { type: "inline-radio" },
10
+ options: ["xs", "s", "md", "lg", "xl"],
11
+ description: "Avatar size",
12
+ table: {
13
+ category: "Avatar",
14
+ },
15
+ },
8
16
  src: {
9
17
  control: { type: "text" },
10
18
  description: "Avatar image source URL",
@@ -61,6 +69,7 @@ export default {
61
69
  },
62
70
  },
63
71
  args: {
72
+ size: "md",
64
73
  src: "https://github.com/srcdev.png",
65
74
  alt: "SrcDev Avatar",
66
75
  chipSize: 12,
@@ -77,7 +86,9 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
77
86
  return { args };
78
87
  },
79
88
  template: `
89
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh;">
80
90
  <StorybookComponent
91
+ :size="args.size"
81
92
  :src="args.src"
82
93
  :alt="args.alt"
83
94
  :chip="{
@@ -88,6 +99,7 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
88
99
  }"
89
100
  :style-class-passthrough="args.styleClassPassthrough"
90
101
  />
102
+ </div>
91
103
  `,
92
104
  });
93
105
 
@@ -0,0 +1,208 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayAvatar from "../DisplayAvatar.vue";
4
+ import DisplayChip from "../../../02.molecules/display-chip/DisplayChip.vue";
5
+
6
+ describe("DisplayAvatar", () => {
7
+ // ─── Mount ───────────────────────────────────────────────────────────────
8
+
9
+ it("mounts without error", async () => {
10
+ const wrapper = await mountSuspended(DisplayAvatar);
11
+ expect(wrapper.vm).toBeTruthy();
12
+ });
13
+
14
+ // ─── Snapshots ───────────────────────────────────────────────────────────
15
+
16
+ it("renders correct HTML structure (default)", async () => {
17
+ const wrapper = await mountSuspended(DisplayAvatar, {
18
+ props: { alt: "John Doe" },
19
+ });
20
+ expect(wrapper.html()).toMatchSnapshot();
21
+ });
22
+
23
+ it("renders correct HTML structure (with src)", async () => {
24
+ const wrapper = await mountSuspended(DisplayAvatar, {
25
+ props: { src: "/avatar.jpg", alt: "John Doe", size: "lg" },
26
+ });
27
+ expect(wrapper.html()).toMatchSnapshot();
28
+ });
29
+
30
+ it("renders correct HTML structure (with chip)", async () => {
31
+ const wrapper = await mountSuspended(DisplayAvatar, {
32
+ props: { alt: "John Doe", chip: true },
33
+ });
34
+ expect(wrapper.html()).toMatchSnapshot();
35
+ });
36
+
37
+ // ─── Root element ─────────────────────────────────────────────────────────
38
+
39
+ it("renders as <span> by default", async () => {
40
+ const wrapper = await mountSuspended(DisplayAvatar);
41
+ expect(wrapper.element.tagName).toBe("SPAN");
42
+ });
43
+
44
+ it("renders as the element specified by the as prop", async () => {
45
+ const wrapper = await mountSuspended(DisplayAvatar, {
46
+ props: { as: "div" },
47
+ });
48
+ expect(wrapper.element.tagName).toBe("DIV");
49
+ });
50
+
51
+ it("renders as DisplayChip when chip prop is set", async () => {
52
+ const wrapper = await mountSuspended(DisplayAvatar, {
53
+ props: { chip: true },
54
+ });
55
+ expect(wrapper.findComponent(DisplayChip).exists()).toBe(true);
56
+ });
57
+
58
+ // ─── Base class ───────────────────────────────────────────────────────────
59
+
60
+ it("always has the display-avatar class", async () => {
61
+ const wrapper = await mountSuspended(DisplayAvatar);
62
+ expect(wrapper.classes()).toContain("display-avatar");
63
+ });
64
+
65
+ // ─── Size ─────────────────────────────────────────────────────────────────
66
+
67
+ it("applies md size class by default", async () => {
68
+ const wrapper = await mountSuspended(DisplayAvatar);
69
+ expect(wrapper.classes()).toContain("md");
70
+ });
71
+
72
+ it.each(["xs", "s", "md", "lg", "xl"] as const)("applies %s size class when size='%s'", async (size) => {
73
+ const wrapper = await mountSuspended(DisplayAvatar, { props: { size } });
74
+ expect(wrapper.classes()).toContain(size);
75
+ });
76
+
77
+ // ─── Fallback text ────────────────────────────────────────────────────────
78
+
79
+ it("shows initials derived from alt when no src or text is set", async () => {
80
+ const wrapper = await mountSuspended(DisplayAvatar, {
81
+ props: { alt: "John Doe" },
82
+ });
83
+ expect(wrapper.find("span").text()).toBe("JD");
84
+ });
85
+
86
+ it("caps initials at two characters", async () => {
87
+ const wrapper = await mountSuspended(DisplayAvatar, {
88
+ props: { alt: "Alice Bob Charlie" },
89
+ });
90
+ expect(wrapper.find("span").text()).toBe("AB");
91
+ });
92
+
93
+ it("shows single initial for a single-word alt", async () => {
94
+ const wrapper = await mountSuspended(DisplayAvatar, {
95
+ props: { alt: "Alice" },
96
+ });
97
+ expect(wrapper.find("span").text()).toBe("A");
98
+ });
99
+
100
+ it("shows text prop instead of alt-derived initials when text is set", async () => {
101
+ const wrapper = await mountSuspended(DisplayAvatar, {
102
+ props: { alt: "John Doe", text: "?" },
103
+ });
104
+ expect(wrapper.find("span").text()).toBe("?");
105
+ });
106
+
107
+ it("shows empty fallback when neither src, text, nor alt are set", async () => {
108
+ const wrapper = await mountSuspended(DisplayAvatar);
109
+ expect(wrapper.find("span").text()).toBe("");
110
+ });
111
+
112
+ // ─── Image ────────────────────────────────────────────────────────────────
113
+
114
+ it("renders an image element when src is provided", async () => {
115
+ const wrapper = await mountSuspended(DisplayAvatar, {
116
+ props: { src: "/avatar.jpg" },
117
+ });
118
+ expect(wrapper.find(".avatar-image").exists()).toBe(true);
119
+ });
120
+
121
+ it("does not render an image element when src is not provided", async () => {
122
+ const wrapper = await mountSuspended(DisplayAvatar);
123
+ expect(wrapper.find(".avatar-image").exists()).toBe(false);
124
+ });
125
+
126
+ it("falls back to 'Avatar' as alt text when src is set but alt is not", async () => {
127
+ const wrapper = await mountSuspended(DisplayAvatar, {
128
+ props: { src: "/avatar.jpg" },
129
+ });
130
+ expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Avatar");
131
+ });
132
+
133
+ it("uses the alt prop as alt text on the image", async () => {
134
+ const wrapper = await mountSuspended(DisplayAvatar, {
135
+ props: { src: "/avatar.jpg", alt: "Profile picture" },
136
+ });
137
+ expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Profile picture");
138
+ });
139
+
140
+ // ─── Slots ────────────────────────────────────────────────────────────────
141
+
142
+ it("renders default slot content instead of the fallback", async () => {
143
+ const wrapper = await mountSuspended(DisplayAvatar, {
144
+ props: { alt: "John Doe" },
145
+ slots: { default: "<img class='custom-avatar' src='/x.jpg' alt='x' />" },
146
+ });
147
+ expect(wrapper.find(".custom-avatar").exists()).toBe(true);
148
+ expect(wrapper.text()).not.toContain("JD");
149
+ });
150
+
151
+ it("renders icon slot content", async () => {
152
+ const wrapper = await mountSuspended(DisplayAvatar, {
153
+ slots: { icon: "<span class='status-icon'>●</span>" },
154
+ });
155
+ expect(wrapper.find(".status-icon").exists()).toBe(true);
156
+ });
157
+
158
+ // ─── Chip ─────────────────────────────────────────────────────────────────
159
+
160
+ it("passes chipDefaultConfig when chip=true", async () => {
161
+ const wrapper = await mountSuspended(DisplayAvatar, {
162
+ props: { chip: true },
163
+ });
164
+ const chip = wrapper.findComponent(DisplayChip);
165
+ expect(chip.props("config")).toMatchObject({
166
+ size: "12px",
167
+ maskWidth: "4px",
168
+ offset: "0px",
169
+ angle: "90deg",
170
+ });
171
+ });
172
+
173
+ it("passes custom config when chip is an object", async () => {
174
+ const chipConfig = { size: "16px", maskWidth: "2px", offset: "4px", angle: "45deg" };
175
+ const wrapper = await mountSuspended(DisplayAvatar, {
176
+ props: { chip: chipConfig },
177
+ });
178
+ const chip = wrapper.findComponent(DisplayChip);
179
+ expect(chip.props("config")).toMatchObject(chipConfig);
180
+ });
181
+
182
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
183
+
184
+ it("applies a single styleClassPassthrough string", async () => {
185
+ const wrapper = await mountSuspended(DisplayAvatar, {
186
+ props: { styleClassPassthrough: "online" },
187
+ });
188
+ expect(wrapper.classes()).toContain("online");
189
+ });
190
+
191
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
192
+ const wrapper = await mountSuspended(DisplayAvatar, {
193
+ props: { styleClassPassthrough: ["online", "featured"] },
194
+ });
195
+ expect(wrapper.classes()).toContain("online");
196
+ expect(wrapper.classes()).toContain("featured");
197
+ });
198
+
199
+ it("updates classes when styleClassPassthrough prop changes", async () => {
200
+ const wrapper = await mountSuspended(DisplayAvatar, {
201
+ props: { styleClassPassthrough: ["online"] },
202
+ });
203
+ expect(wrapper.classes()).toContain("online");
204
+ await wrapper.setProps({ styleClassPassthrough: ["offline"] });
205
+ expect(wrapper.classes()).not.toContain("online");
206
+ expect(wrapper.classes()).toContain("offline");
207
+ });
208
+ });
@@ -0,0 +1,11 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayAvatar > renders correct HTML structure (default) 1`] = `"<span class="display-avatar md" style-class-passthrough=""><span>JD</span></span>"`;
4
+
5
+ exports[`DisplayAvatar > renders correct HTML structure (with chip) 1`] = `
6
+ "<span class="display-chip-core circle display-avatar md" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><span>JD</span>
7
+ <!--v-if-->
8
+ <!--v-if--></span>"
9
+ `;
10
+
11
+ exports[`DisplayAvatar > renders correct HTML structure (with src) 1`] = `"<span class="display-avatar lg" style-class-passthrough=""><img data-nuxt-img="" srcset="/_ipx/_/avatar.jpg 1x, /_ipx/_/avatar.jpg 2x" alt="John Doe" class="avatar-image" src="/_ipx/_/avatar.jpg"></span>"`;
@@ -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>