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.
- package/.claude/settings.json +3 -1
- package/.claude/skills/components/{data-grid.md → auto-grid.md} +36 -34
- package/.claude/skills/components/display-avatar.md +187 -0
- package/.claude/skills/components/display-chip.md +213 -0
- package/.claude/skills/css-animation-utilities.md +87 -0
- package/.claude/skills/index.md +4 -1
- package/app/assets/styles/setup/06.utility-classes/animations/_animation-scroller-x.css +21 -0
- package/app/assets/styles/setup/06.utility-classes/animations/_auto-rotate.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-exit-blur.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/_entry-zoom-reveal.css +5 -3
- package/app/assets/styles/setup/06.utility-classes/animations/index.css +5 -4
- package/app/components/01.atoms/display-avatar/DisplayAvatar.vue +130 -0
- package/app/components/{display-avatar → 01.atoms/display-avatar}/stories/DisplayAvatar.stories.ts +12 -0
- package/app/components/01.atoms/display-avatar/tests/DisplayAvatar.spec.ts +208 -0
- package/app/components/01.atoms/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +11 -0
- package/app/components/01.atoms/grids/data-grid/AutoGrid.vue +57 -0
- package/app/components/01.atoms/grids/data-grid/stories/{DataGrid.stories.ts → AutoGrid.stories.ts} +95 -39
- package/app/components/01.atoms/grids/data-grid/tests/{DataGrid.spec.ts → AutoGrid.spec.ts} +42 -22
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +11 -0
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +3 -3
- package/app/components/02.molecules/display-chip/DisplayChip.vue +189 -0
- package/app/components/{display-chip → 02.molecules/display-chip}/stories/DisplayChip.stories.ts +37 -53
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +191 -0
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +12 -0
- package/app/pages/auto-grid.vue +311 -0
- package/app/pages/index.vue +5 -0
- package/app/pages/ui/display-chip.vue +201 -66
- package/package.json +1 -1
- package/app/components/01.atoms/grids/data-grid/DataGrid.vue +0 -39
- package/app/components/display-avatar/DisplayAvatar.vue +0 -148
- package/app/components/display-chip/DisplayChip.vue +0 -187
package/app/components/{display-chip → 02.molecules/display-chip}/stories/DisplayChip.stories.ts
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
1
2
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
3
|
import StorybookComponent from "../DisplayChip.vue";
|
|
3
|
-
import type { DisplayChipConfig } from "
|
|
4
|
+
import type { DisplayChipConfig } from "~/types/components";
|
|
4
5
|
|
|
5
6
|
// Custom interface for story args
|
|
6
7
|
interface ChipStoryArgs {
|
|
@@ -129,7 +130,7 @@ export default {
|
|
|
129
130
|
label: "",
|
|
130
131
|
status: "offline",
|
|
131
132
|
useSlot: true,
|
|
132
|
-
slotContent: "
|
|
133
|
+
slotContent: "SRC",
|
|
133
134
|
styleClassPassthrough: [],
|
|
134
135
|
},
|
|
135
136
|
} as Meta<typeof StorybookComponent>;
|
|
@@ -137,30 +138,43 @@ export default {
|
|
|
137
138
|
const Template: StoryFn<ChipStoryArgs> = (args) => ({
|
|
138
139
|
components: { StorybookComponent },
|
|
139
140
|
setup() {
|
|
140
|
-
const chipConfig
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
141
|
+
const chipConfig = computed(
|
|
142
|
+
(): DisplayChipConfig => ({
|
|
143
|
+
size: `${args.chipSize}px`,
|
|
144
|
+
maskWidth: `${args.chipMaskWidth}px`,
|
|
145
|
+
offset: `${args.chipOffset}px`,
|
|
146
|
+
angle: `${args.chipAngle}deg`,
|
|
147
|
+
icon: args.icon || undefined,
|
|
148
|
+
label: args.label || undefined,
|
|
149
|
+
})
|
|
150
|
+
);
|
|
148
151
|
|
|
149
|
-
const classes = [...(args.styleClassPassthrough || []), args.status];
|
|
152
|
+
const classes = computed(() => [...(args.styleClassPassthrough || []), args.status]);
|
|
150
153
|
|
|
151
154
|
return { args, chipConfig, classes };
|
|
152
155
|
},
|
|
153
156
|
template: `
|
|
154
|
-
<div style="
|
|
157
|
+
<div style="display: flex; align-items: center; justify-content: center; height: 100vh;">
|
|
155
158
|
<StorybookComponent
|
|
156
159
|
:tag="args.tag"
|
|
157
160
|
:shape="args.shape"
|
|
158
161
|
:config="chipConfig"
|
|
159
162
|
:style-class-passthrough="classes"
|
|
160
|
-
style="width: 100px; height: 100px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white; font-weight: 500;"
|
|
161
163
|
>
|
|
162
164
|
<template v-if="args.useSlot" #default>
|
|
163
|
-
{
|
|
165
|
+
<div :style="{
|
|
166
|
+
width: '50px',
|
|
167
|
+
height: '50px',
|
|
168
|
+
background: '#64748b',
|
|
169
|
+
borderRadius: args.shape === 'circle' ? '50%' : '4px',
|
|
170
|
+
display: 'flex',
|
|
171
|
+
alignItems: 'center',
|
|
172
|
+
justifyContent: 'center',
|
|
173
|
+
color: '#f8fafc',
|
|
174
|
+
fontWeight: '600',
|
|
175
|
+
fontSize: '1.3rem',
|
|
176
|
+
fontFamily: 'sans-serif',
|
|
177
|
+
}">{{ args.slotContent }}</div>
|
|
164
178
|
</template>
|
|
165
179
|
</StorybookComponent>
|
|
166
180
|
</div>
|
|
@@ -179,7 +193,6 @@ export const WithIcon = Template.bind({});
|
|
|
179
193
|
WithIcon.args = {
|
|
180
194
|
status: "online",
|
|
181
195
|
icon: "mdi:check",
|
|
182
|
-
slotContent: "Icon Chip",
|
|
183
196
|
};
|
|
184
197
|
|
|
185
198
|
// Different Statuses
|
|
@@ -257,7 +270,6 @@ SquareShape.args = {
|
|
|
257
270
|
shape: "square",
|
|
258
271
|
status: "online",
|
|
259
272
|
label: "□",
|
|
260
|
-
slotContent: "Square Parent",
|
|
261
273
|
};
|
|
262
274
|
|
|
263
275
|
// With Offset
|
|
@@ -266,7 +278,6 @@ WithOffset.args = {
|
|
|
266
278
|
status: "online",
|
|
267
279
|
chipOffset: 10,
|
|
268
280
|
label: "10",
|
|
269
|
-
slotContent: "Offset Example",
|
|
270
281
|
};
|
|
271
282
|
|
|
272
283
|
// Multiple Chips Demo
|
|
@@ -276,61 +287,34 @@ const MultipleChipsTemplate: StoryFn<ChipStoryArgs> = (args) => ({
|
|
|
276
287
|
return { args };
|
|
277
288
|
},
|
|
278
289
|
template: `
|
|
279
|
-
<div style="
|
|
290
|
+
<div style="display: flex; gap: 40px; align-items: center; justify-content: center; height: 100vh; flex-wrap: wrap;">
|
|
280
291
|
<StorybookComponent
|
|
281
|
-
:config="{
|
|
282
|
-
size: '12px',
|
|
283
|
-
maskWidth: '4px',
|
|
284
|
-
offset: '0px',
|
|
285
|
-
angle: '45deg',
|
|
286
|
-
label: '5'
|
|
287
|
-
}"
|
|
292
|
+
:config="{ size: '12px', maskWidth: '4px', offset: '0px', angle: '45deg', label: '5' }"
|
|
288
293
|
:style-class-passthrough="['online']"
|
|
289
|
-
style="width: 80px; height: 80px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 50%; display: flex; align-items: center; justify-content: center; color: white;"
|
|
290
294
|
>
|
|
291
|
-
|
|
295
|
+
<div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
|
|
292
296
|
</StorybookComponent>
|
|
293
297
|
|
|
294
298
|
<StorybookComponent
|
|
295
|
-
:config="{
|
|
296
|
-
size: '10px',
|
|
297
|
-
maskWidth: '3px',
|
|
298
|
-
offset: '2px',
|
|
299
|
-
angle: '315deg',
|
|
300
|
-
icon: 'mdi:pause'
|
|
301
|
-
}"
|
|
299
|
+
:config="{ size: '10px', maskWidth: '3px', offset: '2px', angle: '315deg', icon: 'mdi:pause' }"
|
|
302
300
|
:style-class-passthrough="['idle']"
|
|
303
|
-
style="width: 80px; height: 80px; background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white;"
|
|
304
301
|
>
|
|
305
|
-
|
|
302
|
+
<div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
|
|
306
303
|
</StorybookComponent>
|
|
307
304
|
|
|
308
305
|
<StorybookComponent
|
|
309
306
|
shape="square"
|
|
310
|
-
:config="{
|
|
311
|
-
size: '14px',
|
|
312
|
-
maskWidth: '2px',
|
|
313
|
-
offset: '-5px',
|
|
314
|
-
angle: '135deg',
|
|
315
|
-
label: 'DND'
|
|
316
|
-
}"
|
|
307
|
+
:config="{ size: '14px', maskWidth: '2px', offset: '-5px', angle: '135deg', label: 'DND' }"
|
|
317
308
|
:style-class-passthrough="['dnd']"
|
|
318
|
-
style="width: 80px; height: 80px; background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); border-radius: 8px; display: flex; align-items: center; justify-content: center; color: white;"
|
|
319
309
|
>
|
|
320
|
-
|
|
310
|
+
<div style="width: 50px; height: 50px; background: #64748b; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
|
|
321
311
|
</StorybookComponent>
|
|
322
312
|
|
|
323
313
|
<StorybookComponent
|
|
324
|
-
:config="{
|
|
325
|
-
size: '16px',
|
|
326
|
-
maskWidth: '6px',
|
|
327
|
-
offset: '8px',
|
|
328
|
-
angle: '90deg'
|
|
329
|
-
}"
|
|
314
|
+
:config="{ size: '16px', maskWidth: '6px', offset: '8px', angle: '90deg' }"
|
|
330
315
|
:style-class-passthrough="['offline']"
|
|
331
|
-
style="width: 80px; height: 80px; background: linear-gradient(135deg, #fa709a 0%, #fee140 100%); border-radius: 16px; display: flex; align-items: center; justify-content: center; color: white;"
|
|
332
316
|
>
|
|
333
|
-
|
|
317
|
+
<div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
|
|
334
318
|
</StorybookComponent>
|
|
335
319
|
</div>
|
|
336
320
|
`,
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayChip from "../DisplayChip.vue";
|
|
4
|
+
|
|
5
|
+
describe("DisplayChip", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
it("renders correct HTML structure (default)", async () => {
|
|
16
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
17
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders correct HTML structure (with label)", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
22
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "5" } },
|
|
23
|
+
});
|
|
24
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("renders correct HTML structure (with icon)", async () => {
|
|
28
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
29
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", icon: "mdi:check" } },
|
|
30
|
+
});
|
|
31
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("renders correct HTML structure (square + styleClassPassthrough)", async () => {
|
|
35
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
36
|
+
props: { shape: "square", styleClassPassthrough: ["online"] },
|
|
37
|
+
});
|
|
38
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
it("renders as <span> by default", async () => {
|
|
44
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
45
|
+
expect(wrapper.element.tagName).toBe("SPAN");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("renders as <div> when tag='div'", async () => {
|
|
49
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { tag: "div" } });
|
|
50
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// ─── Base class ───────────────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
it("always has the display-chip-core class", async () => {
|
|
56
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
57
|
+
expect(wrapper.classes()).toContain("display-chip-core");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// ─── Shape ────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
it("applies circle class by default", async () => {
|
|
63
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
64
|
+
expect(wrapper.classes()).toContain("circle");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("applies square class when shape='square'", async () => {
|
|
68
|
+
const wrapper = await mountSuspended(DisplayChip, { props: { shape: "square" } });
|
|
69
|
+
expect(wrapper.classes()).toContain("square");
|
|
70
|
+
expect(wrapper.classes()).not.toContain("circle");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ─── CSS custom properties ────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
it("sets CSS custom properties from config", async () => {
|
|
76
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
77
|
+
props: {
|
|
78
|
+
config: { size: "16px", maskWidth: "3px", offset: "4px", angle: "45deg" },
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
82
|
+
expect(style.getPropertyValue("--chip-size")).toBe("16px");
|
|
83
|
+
expect(style.getPropertyValue("--chip-mask-width")).toBe("3px");
|
|
84
|
+
expect(style.getPropertyValue("--chip-offset")).toBe("4px");
|
|
85
|
+
expect(style.getPropertyValue("--chip-angle")).toBe("45deg");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("sets default CSS custom properties when no config is provided", async () => {
|
|
89
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
90
|
+
const style = (wrapper.element as HTMLElement).style;
|
|
91
|
+
expect(style.getPropertyValue("--chip-size")).toBe("12px");
|
|
92
|
+
expect(style.getPropertyValue("--chip-mask-width")).toBe("4px");
|
|
93
|
+
expect(style.getPropertyValue("--chip-offset")).toBe("0px");
|
|
94
|
+
expect(style.getPropertyValue("--chip-angle")).toBe("90deg");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// ─── Label ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
it("does not render .chip-label when config has no label", async () => {
|
|
100
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
101
|
+
expect(wrapper.find(".chip-label").exists()).toBe(false);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("renders .chip-label when config.label is set", async () => {
|
|
105
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
106
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "5" } },
|
|
107
|
+
});
|
|
108
|
+
expect(wrapper.find(".chip-label").exists()).toBe(true);
|
|
109
|
+
expect(wrapper.find(".chip-label").text()).toBe("5");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it.each([
|
|
113
|
+
["A", "length-1"],
|
|
114
|
+
["+2", "length-2"],
|
|
115
|
+
["DND", "length-3"],
|
|
116
|
+
])("applies length class for label '%s'", async (label, expectedClass) => {
|
|
117
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
118
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label } },
|
|
119
|
+
});
|
|
120
|
+
expect(wrapper.find(".chip-label").classes()).toContain(expectedClass);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("truncates label to 3 characters when label exceeds maximum length", async () => {
|
|
124
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
125
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
126
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "ABCD" } },
|
|
127
|
+
});
|
|
128
|
+
expect(wrapper.find(".chip-label").text()).toBe("ABC");
|
|
129
|
+
warnSpy.mockRestore();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("emits a console warning when label exceeds 3 characters", async () => {
|
|
133
|
+
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
134
|
+
await mountSuspended(DisplayChip, {
|
|
135
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "ABCD" } },
|
|
136
|
+
});
|
|
137
|
+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("exceeds maximum length of 3 characters"));
|
|
138
|
+
warnSpy.mockRestore();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// ─── Icon ─────────────────────────────────────────────────────────────────
|
|
142
|
+
|
|
143
|
+
it("does not render .chip-icon when config has no icon", async () => {
|
|
144
|
+
const wrapper = await mountSuspended(DisplayChip);
|
|
145
|
+
expect(wrapper.find(".chip-icon").exists()).toBe(false);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("renders .chip-icon when config.icon is set", async () => {
|
|
149
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
150
|
+
props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", icon: "mdi:check" } },
|
|
151
|
+
});
|
|
152
|
+
expect(wrapper.find(".chip-icon").exists()).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ─── Slot ─────────────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
it("renders default slot content", async () => {
|
|
158
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
159
|
+
slots: { default: "<div class='avatar'>SRC</div>" },
|
|
160
|
+
});
|
|
161
|
+
expect(wrapper.find(".avatar").exists()).toBe(true);
|
|
162
|
+
expect(wrapper.find(".avatar").text()).toBe("SRC");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
168
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
169
|
+
props: { styleClassPassthrough: "online" },
|
|
170
|
+
});
|
|
171
|
+
expect(wrapper.classes()).toContain("online");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
175
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
176
|
+
props: { styleClassPassthrough: ["online", "featured"] },
|
|
177
|
+
});
|
|
178
|
+
expect(wrapper.classes()).toContain("online");
|
|
179
|
+
expect(wrapper.classes()).toContain("featured");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
183
|
+
const wrapper = await mountSuspended(DisplayChip, {
|
|
184
|
+
props: { styleClassPassthrough: ["online"] },
|
|
185
|
+
});
|
|
186
|
+
expect(wrapper.classes()).toContain("online");
|
|
187
|
+
await wrapper.setProps({ styleClassPassthrough: ["idle"] });
|
|
188
|
+
expect(wrapper.classes()).not.toContain("online");
|
|
189
|
+
expect(wrapper.classes()).toContain("idle");
|
|
190
|
+
});
|
|
191
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DisplayChip > renders correct HTML structure (default) 1`] = `"<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><!--v-if--></span>"`;
|
|
4
|
+
|
|
5
|
+
exports[`DisplayChip > renders correct HTML structure (square + styleClassPassthrough) 1`] = `"<span class="display-chip-core square online" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><!--v-if--></span>"`;
|
|
6
|
+
|
|
7
|
+
exports[`DisplayChip > renders correct HTML structure (with icon) 1`] = `
|
|
8
|
+
"<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><span class="iconify i-mdi:check chip-icon" aria-hidden="true"></span>
|
|
9
|
+
<!--v-if--></span>"
|
|
10
|
+
`;
|
|
11
|
+
|
|
12
|
+
exports[`DisplayChip > renders correct HTML structure (with label) 1`] = `"<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><span class="chip-label length-1">5</span></span>"`;
|
|
@@ -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>
|