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/01.atoms/grids/data-grid/stories/{DataGrid.stories.ts → AutoGrid.stories.ts}
RENAMED
|
@@ -1,15 +1,53 @@
|
|
|
1
|
+
import { computed } from "vue";
|
|
1
2
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
-
import
|
|
3
|
+
import AutoGridComponent from "../AutoGrid.vue";
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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: "
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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: {
|
|
150
|
+
components: { AutoGridComponent },
|
|
95
151
|
template: `
|
|
96
|
-
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
97
|
-
<
|
|
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
|
-
</
|
|
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<
|
|
120
|
-
components: {
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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<
|
|
160
|
-
components: {
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
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<
|
|
199
|
-
components: {
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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
|
|
3
|
+
import AutoGrid from "../AutoGrid.vue";
|
|
4
4
|
|
|
5
|
-
describe("
|
|
5
|
+
describe("AutoGrid", () => {
|
|
6
6
|
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
7
|
|
|
8
8
|
it("mounts without error", async () => {
|
|
9
|
-
const wrapper = await mountSuspended(
|
|
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(
|
|
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(
|
|
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(
|
|
39
|
+
const wrapper = await mountSuspended(AutoGrid);
|
|
39
40
|
expect(wrapper.element.tagName).toBe("DIV");
|
|
40
41
|
});
|
|
41
42
|
|
|
42
|
-
it("always has the
|
|
43
|
-
const wrapper = await mountSuspended(
|
|
44
|
-
expect(wrapper.classes()).toContain("
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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[`
|
|
4
|
-
"<section class="
|
|
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[`
|
|
11
|
+
exports[`AutoGrid > renders correct HTML structure (default props) 1`] = `"<div class="auto-grid"></div>"`;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<component :is="tag" class="display-chip-core" :class="[shape, elementClasses]" :style="chipStyles">
|
|
3
|
+
<slot name="default"></slot>
|
|
4
|
+
<Icon v-if="config?.icon" :name="config.icon" class="chip-icon" />
|
|
5
|
+
<span v-if="config?.label" class="chip-label" :class="`length-${config.label.length}`">{{ validatedLabel }}</span>
|
|
6
|
+
</component>
|
|
7
|
+
</template>
|
|
8
|
+
|
|
9
|
+
<script setup lang="ts">
|
|
10
|
+
import type { DisplayChipConfig } from "~/types/components";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
tag?: "div" | "span";
|
|
14
|
+
shape?: "circle" | "square";
|
|
15
|
+
config?: DisplayChipConfig;
|
|
16
|
+
styleClassPassthrough?: string | string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
20
|
+
tag: "span",
|
|
21
|
+
shape: "circle",
|
|
22
|
+
config: () => ({
|
|
23
|
+
size: "12px",
|
|
24
|
+
maskWidth: "4px",
|
|
25
|
+
offset: "0px",
|
|
26
|
+
angle: "90deg",
|
|
27
|
+
icon: undefined,
|
|
28
|
+
label: undefined,
|
|
29
|
+
}),
|
|
30
|
+
styleClassPassthrough: () => [],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
34
|
+
|
|
35
|
+
watch(
|
|
36
|
+
() => props.styleClassPassthrough,
|
|
37
|
+
() => {
|
|
38
|
+
resetElementClasses(props.styleClassPassthrough);
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const validatedLabel = computed(() => {
|
|
43
|
+
if (!props.config?.label) return props.config?.label;
|
|
44
|
+
if (props.config.label.length > 3) {
|
|
45
|
+
console.warn(
|
|
46
|
+
`DisplayChip: label "${
|
|
47
|
+
props.config.label
|
|
48
|
+
}" exceeds maximum length of 3 characters. Truncating to "${props.config.label.slice(0, 3)}"`
|
|
49
|
+
);
|
|
50
|
+
return props.config.label.slice(0, 3);
|
|
51
|
+
}
|
|
52
|
+
return props.config.label;
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const chipStyles = computed(() => ({
|
|
56
|
+
"--chip-size": props.config?.size,
|
|
57
|
+
"--chip-mask-width": props.config?.maskWidth,
|
|
58
|
+
"--chip-offset": props.config?.offset,
|
|
59
|
+
"--chip-angle": props.config?.angle,
|
|
60
|
+
}));
|
|
61
|
+
</script>
|
|
62
|
+
|
|
63
|
+
<style lang="css">
|
|
64
|
+
@layer components {
|
|
65
|
+
.display-chip-core {
|
|
66
|
+
--computed-mask-diameter: calc(var(--chip-size) + (var(--chip-mask-width) * 2));
|
|
67
|
+
|
|
68
|
+
&.circle {
|
|
69
|
+
--computed-chip-offset: calc((100% / 2) + var(--chip-offset));
|
|
70
|
+
--computed-position-x: calc(var(--computed-chip-offset) * cos(var(--chip-angle) - 90deg) + (100% / 2));
|
|
71
|
+
--computed-position-y: calc(var(--computed-chip-offset) * sin(var(--chip-angle) - 90deg) + (100% / 2));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
&.square {
|
|
75
|
+
--circle-x: calc(50% + (50% + var(--chip-offset) + (var(--chip-size) / 2)) * cos(var(--chip-angle) - 90deg));
|
|
76
|
+
--circle-y: calc(50% + (50% + var(--chip-offset) + (var(--chip-size) / 2)) * sin(var(--chip-angle) - 90deg));
|
|
77
|
+
--computed-position-x: clamp(calc(var(--chip-offset) * -1), var(--circle-x), calc(100% + var(--chip-offset)));
|
|
78
|
+
--computed-position-y: clamp(calc(var(--chip-offset) * -1), var(--circle-y), calc(100% + var(--chip-offset)));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* colors */
|
|
82
|
+
|
|
83
|
+
--color-offline: slategrey;
|
|
84
|
+
--color-online: rgb(0, 255, 135);
|
|
85
|
+
--color-idle: rgb(255, 185, 51);
|
|
86
|
+
--color-dnd: rgb(255, 40, 80);
|
|
87
|
+
|
|
88
|
+
position: relative;
|
|
89
|
+
display: inline-block;
|
|
90
|
+
|
|
91
|
+
&::after {
|
|
92
|
+
content: "";
|
|
93
|
+
aspect-ratio: 1;
|
|
94
|
+
background: var(--color-offline);
|
|
95
|
+
position: absolute;
|
|
96
|
+
width: var(--chip-size);
|
|
97
|
+
border-radius: 100%;
|
|
98
|
+
z-index: 1;
|
|
99
|
+
}
|
|
100
|
+
.chip-icon {
|
|
101
|
+
position: absolute;
|
|
102
|
+
width: var(--chip-size);
|
|
103
|
+
height: var(--chip-size);
|
|
104
|
+
border-radius: 100%;
|
|
105
|
+
display: flex;
|
|
106
|
+
align-items: center;
|
|
107
|
+
justify-content: center;
|
|
108
|
+
color: black;
|
|
109
|
+
z-index: 2;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.chip-label {
|
|
113
|
+
--_font-size-adjust: 0.7;
|
|
114
|
+
position: absolute;
|
|
115
|
+
width: var(--chip-size);
|
|
116
|
+
height: var(--chip-size);
|
|
117
|
+
border-radius: 100%;
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
color: black;
|
|
122
|
+
z-index: 2;
|
|
123
|
+
font-size: calc(var(--chip-size) * var(--_font-size-adjust));
|
|
124
|
+
line-height: 1;
|
|
125
|
+
letter-spacing: -0.05rem;
|
|
126
|
+
user-select: none;
|
|
127
|
+
|
|
128
|
+
&.length-2 {
|
|
129
|
+
--_font-size-adjust: 0.6;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
&.length-3 {
|
|
133
|
+
--_font-size-adjust: 0.5;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
& > * {
|
|
138
|
+
/*
|
|
139
|
+
create the cutout mask around the image,
|
|
140
|
+
it's just a radial gradient positioned at the same place as the
|
|
141
|
+
psuedo-element ::after
|
|
142
|
+
*/
|
|
143
|
+
|
|
144
|
+
&:not(.chip-icon, .chip-label) {
|
|
145
|
+
mask-image: radial-gradient(
|
|
146
|
+
var(--computed-mask-diameter) var(--computed-mask-diameter) at var(--computed-position-x)
|
|
147
|
+
var(--computed-position-y),
|
|
148
|
+
transparent calc(50% - 0.5px),
|
|
149
|
+
black calc(50% + 0.5px)
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
&.circle {
|
|
155
|
+
&::after,
|
|
156
|
+
.chip-icon,
|
|
157
|
+
.chip-label {
|
|
158
|
+
top: calc(var(--computed-position-y) - (var(--chip-size) / 2));
|
|
159
|
+
left: calc(var(--computed-position-x) - (var(--chip-size) / 2));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
&.square {
|
|
164
|
+
&::after,
|
|
165
|
+
.chip-icon,
|
|
166
|
+
.chip-label {
|
|
167
|
+
top: calc(var(--computed-position-y) - (var(--chip-size) / 2));
|
|
168
|
+
left: calc(var(--computed-position-x) - (var(--chip-size) / 2));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
&.online {
|
|
173
|
+
&::after {
|
|
174
|
+
background-color: var(--color-online);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
&.idle {
|
|
178
|
+
&::after {
|
|
179
|
+
background-color: var(--color-idle);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
&.dnd {
|
|
183
|
+
&::after {
|
|
184
|
+
background-color: var(--color-dnd);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
</style>
|