srcdev-nuxt-components 9.1.36 → 9.1.38
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 +2 -1
- package/.claude/settings.local.json +2 -2
- package/.claude/skills/components/capture-qr-code.md +84 -0
- package/.claude/skills/components/data-grid.md +167 -0
- package/.claude/skills/components/decode-qr-code.md +84 -0
- package/.claude/skills/components/display-qr-code.md +64 -0
- package/.claude/skills/index.md +5 -2
- package/app/components/01.atoms/grids/data-grid/DataGrid.vue +39 -0
- package/app/components/01.atoms/grids/data-grid/stories/DataGrid.stories.ts +234 -0
- package/app/components/01.atoms/grids/data-grid/tests/DataGrid.spec.ts +140 -0
- package/app/components/01.atoms/grids/data-grid/tests/__snapshots__/DataGrid.spec.ts.snap +11 -0
- package/app/components/01.atoms/qr-code/DisplayQrCode.vue +50 -0
- package/app/components/01.atoms/qr-code/stories/DisplayQrCode.stories.ts +206 -0
- package/app/components/01.atoms/qr-code/tests/DisplayQrCode.spec.ts +139 -0
- package/app/components/02.molecules/qr-code/CaptureQrCode.vue +142 -0
- package/app/components/{qr-code → 02.molecules/qr-code}/DecodeQrCode.vue +11 -35
- package/app/components/02.molecules/qr-code/stories/QrCode.stories.ts +101 -0
- package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +212 -0
- package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +145 -0
- package/app/layouts/default.vue +0 -1
- package/app/pages/ui/qr-code/[componentName].vue +3 -3
- package/app/pages/ui/simple-grid.vue +2 -2
- package/package.json +1 -1
- package/.claude/skills/components/scroll-parallax-section.md +0 -148
- package/app/components/01.atoms/scroll-parallax-section/ScrollParallaxSection.vue +0 -108
- package/app/components/01.atoms/scroll-parallax-section/stories/ScrollParallaxSection.stories.ts +0 -151
- package/app/components/01.atoms/scroll-parallax-section/tests/ScrollParallaxSection.spec.ts +0 -91
- package/app/components/display-grid/DisplayGridCore.vue +0 -22
- package/app/components/qr-code/CaptureQrCode.vue +0 -183
- package/app/components/qr-code/DisplayQrCode.vue +0 -53
- package/app/components/qr-code/stories/QrCode.stories.ts +0 -933
- package/app/pages/ui/scroll-parallax-section.vue +0 -65
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import DataGridComponent from "../DataGrid.vue";
|
|
3
|
+
|
|
4
|
+
const meta: Meta<typeof DataGridComponent> = {
|
|
5
|
+
title: "Atoms/Grids/DataGrid",
|
|
6
|
+
component: DataGridComponent,
|
|
7
|
+
argTypes: {
|
|
8
|
+
tag: {
|
|
9
|
+
control: { type: "select" },
|
|
10
|
+
options: ["div", "section", "article", "main"],
|
|
11
|
+
description: "HTML tag to render as",
|
|
12
|
+
table: { category: "Semantic" },
|
|
13
|
+
},
|
|
14
|
+
styleClassPassthrough: {
|
|
15
|
+
table: { disable: true },
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
args: {
|
|
19
|
+
tag: "div",
|
|
20
|
+
styleClassPassthrough: [],
|
|
21
|
+
},
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
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.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default meta;
|
|
33
|
+
|
|
34
|
+
const cardStyle =
|
|
35
|
+
"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;";
|
|
37
|
+
const valueStyle = "font-size: 2.4rem; font-weight: 700; color: #111827;";
|
|
38
|
+
|
|
39
|
+
interface DataGridArgs {
|
|
40
|
+
tag: "div" | "section" | "article" | "main";
|
|
41
|
+
styleClassPassthrough: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const Template: StoryFn<DataGridArgs> = (args) => ({
|
|
45
|
+
components: { DataGridComponent },
|
|
46
|
+
setup() {
|
|
47
|
+
return { args };
|
|
48
|
+
},
|
|
49
|
+
template: `
|
|
50
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
51
|
+
<DataGridComponent :tag="args.tag" :style-class-passthrough="args.styleClassPassthrough">
|
|
52
|
+
<template #item-1>
|
|
53
|
+
<div style="${cardStyle}">
|
|
54
|
+
<span style="${labelStyle}">Revenue</span>
|
|
55
|
+
<span style="${valueStyle}">£24,500</span>
|
|
56
|
+
</div>
|
|
57
|
+
</template>
|
|
58
|
+
<template #item-2>
|
|
59
|
+
<div style="${cardStyle}">
|
|
60
|
+
<span style="${labelStyle}">Clients</span>
|
|
61
|
+
<span style="${valueStyle}">142</span>
|
|
62
|
+
</div>
|
|
63
|
+
</template>
|
|
64
|
+
<template #item-3>
|
|
65
|
+
<div style="${cardStyle}">
|
|
66
|
+
<span style="${labelStyle}">Bookings</span>
|
|
67
|
+
<span style="${valueStyle}">38</span>
|
|
68
|
+
</div>
|
|
69
|
+
</template>
|
|
70
|
+
<template #item-4>
|
|
71
|
+
<div style="${cardStyle}">
|
|
72
|
+
<span style="${labelStyle}">Avg. Rating</span>
|
|
73
|
+
<span style="${valueStyle}">4.9</span>
|
|
74
|
+
</div>
|
|
75
|
+
</template>
|
|
76
|
+
</DataGridComponent>
|
|
77
|
+
</div>
|
|
78
|
+
`,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export const Default = Template.bind({});
|
|
82
|
+
Default.parameters = {
|
|
83
|
+
docs: {
|
|
84
|
+
description: {
|
|
85
|
+
story: "Default grid with four stat cards. Columns auto-fit to available space at a minimum of 250px each.",
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export const TwoItems = Template.bind({});
|
|
91
|
+
TwoItems.storyName = "Two Items";
|
|
92
|
+
TwoItems.decorators = [
|
|
93
|
+
() => ({
|
|
94
|
+
components: { DataGridComponent },
|
|
95
|
+
template: `
|
|
96
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
97
|
+
<DataGridComponent>
|
|
98
|
+
<template #item-1>
|
|
99
|
+
<div style="${cardStyle}">
|
|
100
|
+
<span style="${labelStyle}">Revenue</span>
|
|
101
|
+
<span style="${valueStyle}">£24,500</span>
|
|
102
|
+
</div>
|
|
103
|
+
</template>
|
|
104
|
+
<template #item-2>
|
|
105
|
+
<div style="${cardStyle}">
|
|
106
|
+
<span style="${labelStyle}">Clients</span>
|
|
107
|
+
<span style="${valueStyle}">142</span>
|
|
108
|
+
</div>
|
|
109
|
+
</template>
|
|
110
|
+
</DataGridComponent>
|
|
111
|
+
</div>
|
|
112
|
+
`,
|
|
113
|
+
}),
|
|
114
|
+
];
|
|
115
|
+
TwoItems.parameters = {
|
|
116
|
+
docs: { description: { story: "Grid with two items — auto-fit spreads them to fill available columns." } },
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export const NarrowContainer: StoryFn<DataGridArgs> = (args) => ({
|
|
120
|
+
components: { DataGridComponent },
|
|
121
|
+
setup() {
|
|
122
|
+
return { args };
|
|
123
|
+
},
|
|
124
|
+
template: `
|
|
125
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
126
|
+
<div style="max-width: 400px;">
|
|
127
|
+
<DataGridComponent :tag="args.tag">
|
|
128
|
+
<template #item-1>
|
|
129
|
+
<div style="${cardStyle}">
|
|
130
|
+
<span style="${labelStyle}">Revenue</span>
|
|
131
|
+
<span style="${valueStyle}">£24,500</span>
|
|
132
|
+
</div>
|
|
133
|
+
</template>
|
|
134
|
+
<template #item-2>
|
|
135
|
+
<div style="${cardStyle}">
|
|
136
|
+
<span style="${labelStyle}">Clients</span>
|
|
137
|
+
<span style="${valueStyle}">142</span>
|
|
138
|
+
</div>
|
|
139
|
+
</template>
|
|
140
|
+
<template #item-3>
|
|
141
|
+
<div style="${cardStyle}">
|
|
142
|
+
<span style="${labelStyle}">Bookings</span>
|
|
143
|
+
<span style="${valueStyle}">38</span>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
</DataGridComponent>
|
|
147
|
+
</div>
|
|
148
|
+
</div>
|
|
149
|
+
`,
|
|
150
|
+
});
|
|
151
|
+
NarrowContainer.parameters = {
|
|
152
|
+
docs: {
|
|
153
|
+
description: {
|
|
154
|
+
story: "In a 400px container the auto-fit columns stack to a single column once items fall below 250px.",
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
export const CustomColumns: StoryFn<DataGridArgs> = (args) => ({
|
|
160
|
+
components: { DataGridComponent },
|
|
161
|
+
setup() {
|
|
162
|
+
return { args };
|
|
163
|
+
},
|
|
164
|
+
template: `
|
|
165
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
166
|
+
<DataGridComponent style="--data-grid-columns: repeat(3, 1fr); --data-grid-gap: 2.4rem;" :tag="args.tag">
|
|
167
|
+
<template #item-1>
|
|
168
|
+
<div style="${cardStyle}">
|
|
169
|
+
<span style="${labelStyle}">Revenue</span>
|
|
170
|
+
<span style="${valueStyle}">£24,500</span>
|
|
171
|
+
</div>
|
|
172
|
+
</template>
|
|
173
|
+
<template #item-2>
|
|
174
|
+
<div style="${cardStyle}">
|
|
175
|
+
<span style="${labelStyle}">Clients</span>
|
|
176
|
+
<span style="${valueStyle}">142</span>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
<template #item-3>
|
|
180
|
+
<div style="${cardStyle}">
|
|
181
|
+
<span style="${labelStyle}">Bookings</span>
|
|
182
|
+
<span style="${valueStyle}">38</span>
|
|
183
|
+
</div>
|
|
184
|
+
</template>
|
|
185
|
+
</DataGridComponent>
|
|
186
|
+
</div>
|
|
187
|
+
`,
|
|
188
|
+
});
|
|
189
|
+
CustomColumns.parameters = {
|
|
190
|
+
docs: {
|
|
191
|
+
description: {
|
|
192
|
+
story:
|
|
193
|
+
"Override `--data-grid-columns` and `--data-grid-gap` via inline style to force a fixed 3-column layout with wider gaps.",
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
export const SemanticSection: StoryFn<DataGridArgs> = (args) => ({
|
|
199
|
+
components: { DataGridComponent },
|
|
200
|
+
setup() {
|
|
201
|
+
return { args };
|
|
202
|
+
},
|
|
203
|
+
template: `
|
|
204
|
+
<div style="padding: 3.2rem; background: #f9fafb;">
|
|
205
|
+
<DataGridComponent tag="section">
|
|
206
|
+
<template #item-1>
|
|
207
|
+
<div style="${cardStyle}">
|
|
208
|
+
<span style="${labelStyle}">Revenue</span>
|
|
209
|
+
<span style="${valueStyle}">£24,500</span>
|
|
210
|
+
</div>
|
|
211
|
+
</template>
|
|
212
|
+
<template #item-2>
|
|
213
|
+
<div style="${cardStyle}">
|
|
214
|
+
<span style="${labelStyle}">Clients</span>
|
|
215
|
+
<span style="${valueStyle}">142</span>
|
|
216
|
+
</div>
|
|
217
|
+
</template>
|
|
218
|
+
<template #item-3>
|
|
219
|
+
<div style="${cardStyle}">
|
|
220
|
+
<span style="${labelStyle}">Bookings</span>
|
|
221
|
+
<span style="${valueStyle}">38</span>
|
|
222
|
+
</div>
|
|
223
|
+
</template>
|
|
224
|
+
</DataGridComponent>
|
|
225
|
+
</div>
|
|
226
|
+
`,
|
|
227
|
+
});
|
|
228
|
+
SemanticSection.parameters = {
|
|
229
|
+
docs: {
|
|
230
|
+
description: {
|
|
231
|
+
story: "Rendered as a `<section>` — `aria-labelledby` is automatically applied via `useAriaLabelledById`.",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DataGrid from "../DataGrid.vue";
|
|
4
|
+
|
|
5
|
+
describe("DataGrid", () => {
|
|
6
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it("mounts without error", async () => {
|
|
9
|
+
const wrapper = await mountSuspended(DataGrid);
|
|
10
|
+
expect(wrapper.vm).toBeTruthy();
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
it("renders correct HTML structure (default props)", async () => {
|
|
16
|
+
const wrapper = await mountSuspended(DataGrid);
|
|
17
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("renders correct HTML structure (all props and slots set)", async () => {
|
|
21
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
22
|
+
props: {
|
|
23
|
+
tag: "section",
|
|
24
|
+
styleClassPassthrough: ["custom-class"],
|
|
25
|
+
},
|
|
26
|
+
slots: {
|
|
27
|
+
"item-1": "<div>Item 1</div>",
|
|
28
|
+
"item-2": "<div>Item 2</div>",
|
|
29
|
+
"item-3": "<div>Item 3</div>",
|
|
30
|
+
},
|
|
31
|
+
});
|
|
32
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// ─── Root element ────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
it("renders a <div> as the root element by default", async () => {
|
|
38
|
+
const wrapper = await mountSuspended(DataGrid);
|
|
39
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("always has the data-grid class", async () => {
|
|
43
|
+
const wrapper = await mountSuspended(DataGrid);
|
|
44
|
+
expect(wrapper.classes()).toContain("data-grid");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// ─── Tag prop ────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
it("renders a <section> when tag is section", async () => {
|
|
50
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "section" } });
|
|
51
|
+
expect(wrapper.element.tagName).toBe("SECTION");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("renders an <article> when tag is article", async () => {
|
|
55
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "article" } });
|
|
56
|
+
expect(wrapper.element.tagName).toBe("ARTICLE");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("renders a <main> when tag is main", async () => {
|
|
60
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "main" } });
|
|
61
|
+
expect(wrapper.element.tagName).toBe("MAIN");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// ─── Aria ─────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
it("does not set aria-labelledby when tag is div", async () => {
|
|
67
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "div" } });
|
|
68
|
+
expect(wrapper.attributes("aria-labelledby")).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("sets aria-labelledby when tag is section", async () => {
|
|
72
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "section" } });
|
|
73
|
+
expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("sets aria-labelledby when tag is article", async () => {
|
|
77
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "article" } });
|
|
78
|
+
expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("sets aria-labelledby when tag is main", async () => {
|
|
82
|
+
const wrapper = await mountSuspended(DataGrid, { props: { tag: "main" } });
|
|
83
|
+
expect(wrapper.attributes("aria-labelledby")).toBeTruthy();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ─── Slots ───────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
it("renders a named slot", async () => {
|
|
89
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
90
|
+
slots: { "item-1": "<div class='card'>Card 1</div>" },
|
|
91
|
+
});
|
|
92
|
+
expect(wrapper.find(".card").exists()).toBe(true);
|
|
93
|
+
expect(wrapper.find(".card").text()).toBe("Card 1");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("renders multiple named slots", async () => {
|
|
97
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
98
|
+
slots: {
|
|
99
|
+
"item-1": "<div class='card-1'>Card 1</div>",
|
|
100
|
+
"item-2": "<div class='card-2'>Card 2</div>",
|
|
101
|
+
"item-3": "<div class='card-3'>Card 3</div>",
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(wrapper.find(".card-1").exists()).toBe(true);
|
|
105
|
+
expect(wrapper.find(".card-2").exists()).toBe(true);
|
|
106
|
+
expect(wrapper.find(".card-3").exists()).toBe(true);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("renders no slot content when no slots are provided", async () => {
|
|
110
|
+
const wrapper = await mountSuspended(DataGrid);
|
|
111
|
+
expect(wrapper.text()).toBe("");
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// ─── styleClassPassthrough ───────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
it("applies a single styleClassPassthrough string to the root", async () => {
|
|
117
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
118
|
+
props: { styleClassPassthrough: "my-grid" },
|
|
119
|
+
});
|
|
120
|
+
expect(wrapper.classes()).toContain("my-grid");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
124
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
125
|
+
props: { styleClassPassthrough: ["my-grid", "mbe-32"] },
|
|
126
|
+
});
|
|
127
|
+
expect(wrapper.classes()).toContain("my-grid");
|
|
128
|
+
expect(wrapper.classes()).toContain("mbe-32");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
132
|
+
const wrapper = await mountSuspended(DataGrid, {
|
|
133
|
+
props: { styleClassPassthrough: ["original"] },
|
|
134
|
+
});
|
|
135
|
+
expect(wrapper.classes()).toContain("original");
|
|
136
|
+
await wrapper.setProps({ styleClassPassthrough: ["updated"] });
|
|
137
|
+
expect(wrapper.classes()).not.toContain("original");
|
|
138
|
+
expect(wrapper.classes()).toContain("updated");
|
|
139
|
+
});
|
|
140
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
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">
|
|
5
|
+
<div>Item 1</div>
|
|
6
|
+
<div>Item 2</div>
|
|
7
|
+
<div>Item 3</div>
|
|
8
|
+
</section>"
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`DataGrid > renders correct HTML structure (default props) 1`] = `"<div class="data-grid"></div>"`;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<Qrcode
|
|
3
|
+
:value="qrValue"
|
|
4
|
+
class="display-qr-code"
|
|
5
|
+
:variant
|
|
6
|
+
:radius
|
|
7
|
+
:black-color
|
|
8
|
+
:white-color
|
|
9
|
+
:class="[elementClasses]"
|
|
10
|
+
/>
|
|
11
|
+
</template>
|
|
12
|
+
|
|
13
|
+
<script setup lang="ts">
|
|
14
|
+
import type { QrCodeVariant } from "~/types/components";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
qrValue: string;
|
|
18
|
+
variant?: QrCodeVariant;
|
|
19
|
+
radius?: number;
|
|
20
|
+
blackColor?: string;
|
|
21
|
+
whiteColor?: string;
|
|
22
|
+
size?: string;
|
|
23
|
+
styleClassPassthrough?: string | string[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
27
|
+
variant: () => ({ inner: "default", marker: "default", pixel: "default" }),
|
|
28
|
+
radius: 0,
|
|
29
|
+
blackColor: "currentColor",
|
|
30
|
+
whiteColor: "transparent",
|
|
31
|
+
size: "256px",
|
|
32
|
+
styleClassPassthrough: () => [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
36
|
+
|
|
37
|
+
watch(
|
|
38
|
+
() => props.styleClassPassthrough,
|
|
39
|
+
() => resetElementClasses(props.styleClassPassthrough)
|
|
40
|
+
);
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<style lang="css">
|
|
44
|
+
@layer components {
|
|
45
|
+
.display-qr-code {
|
|
46
|
+
aspect-ratio: 1 / 1;
|
|
47
|
+
width: v-bind(size);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
</style>
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import DisplayQrCodeComponent from "../DisplayQrCode.vue";
|
|
3
|
+
import type { QrCodeVariant } from "../../../../types/components";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof DisplayQrCodeComponent> = {
|
|
6
|
+
title: "Atoms/QR Code/DisplayQrCode",
|
|
7
|
+
component: DisplayQrCodeComponent,
|
|
8
|
+
argTypes: {
|
|
9
|
+
qrValue: {
|
|
10
|
+
control: { type: "text" },
|
|
11
|
+
description: "The value/content to encode in the QR code",
|
|
12
|
+
table: { category: "Content" },
|
|
13
|
+
},
|
|
14
|
+
size: {
|
|
15
|
+
control: { type: "text" },
|
|
16
|
+
description: "Size of the QR code (CSS value like '256px', '10rem')",
|
|
17
|
+
table: { category: "Appearance" },
|
|
18
|
+
},
|
|
19
|
+
radius: {
|
|
20
|
+
control: { type: "range", min: 0, max: 20, step: 1 },
|
|
21
|
+
description: "Border radius of the QR code corners",
|
|
22
|
+
table: { category: "Appearance" },
|
|
23
|
+
},
|
|
24
|
+
blackColor: {
|
|
25
|
+
control: { type: "color" },
|
|
26
|
+
description: "Color of the QR code foreground elements",
|
|
27
|
+
table: { category: "Appearance" },
|
|
28
|
+
},
|
|
29
|
+
whiteColor: {
|
|
30
|
+
control: { type: "color" },
|
|
31
|
+
description: "Color of the QR code background",
|
|
32
|
+
table: { category: "Appearance" },
|
|
33
|
+
},
|
|
34
|
+
variant: { table: { disable: true } },
|
|
35
|
+
styleClassPassthrough: { table: { disable: true } },
|
|
36
|
+
},
|
|
37
|
+
args: {
|
|
38
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
39
|
+
size: "256px",
|
|
40
|
+
radius: 0,
|
|
41
|
+
blackColor: "#000000",
|
|
42
|
+
whiteColor: "transparent",
|
|
43
|
+
styleClassPassthrough: [],
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export default meta;
|
|
48
|
+
|
|
49
|
+
interface QrStoryArgs {
|
|
50
|
+
qrValue: string;
|
|
51
|
+
size: string;
|
|
52
|
+
radius: number;
|
|
53
|
+
blackColor: string;
|
|
54
|
+
whiteColor: string;
|
|
55
|
+
styleClassPassthrough: string[];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DisplayTemplate: StoryFn<QrStoryArgs> = (args, { parameters }) => ({
|
|
59
|
+
components: { DisplayQrCodeComponent },
|
|
60
|
+
setup() {
|
|
61
|
+
const variant: QrCodeVariant = parameters?.variant || {
|
|
62
|
+
inner: "default",
|
|
63
|
+
marker: "default",
|
|
64
|
+
pixel: "default",
|
|
65
|
+
};
|
|
66
|
+
return { args, variant };
|
|
67
|
+
},
|
|
68
|
+
template: `
|
|
69
|
+
<div style="padding: 40px; display: flex; align-items: center; justify-content: center; background: #f5f5f5; border-radius: 8px;">
|
|
70
|
+
<DisplayQrCodeComponent
|
|
71
|
+
:qrValue="args.qrValue"
|
|
72
|
+
:variant="variant"
|
|
73
|
+
:radius="args.radius"
|
|
74
|
+
:blackColor="args.blackColor"
|
|
75
|
+
:whiteColor="args.whiteColor"
|
|
76
|
+
:size="args.size"
|
|
77
|
+
:style-class-passthrough="args.styleClassPassthrough"
|
|
78
|
+
/>
|
|
79
|
+
</div>
|
|
80
|
+
`,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
export const Default = DisplayTemplate.bind({});
|
|
84
|
+
|
|
85
|
+
export const Website = DisplayTemplate.bind({});
|
|
86
|
+
Website.args = { qrValue: "https://nuxt.com", radius: 8, blackColor: "#000000" };
|
|
87
|
+
Website.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
88
|
+
|
|
89
|
+
export const VariantDefault = DisplayTemplate.bind({});
|
|
90
|
+
VariantDefault.args = { qrValue: "Default variant style", radius: 4 };
|
|
91
|
+
VariantDefault.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
92
|
+
|
|
93
|
+
export const VariantCircle = DisplayTemplate.bind({});
|
|
94
|
+
VariantCircle.args = { qrValue: "Circle variant style", radius: 8, blackColor: "#1e40af" };
|
|
95
|
+
VariantCircle.parameters = { variant: { inner: "circle", marker: "circle", pixel: "circle" } };
|
|
96
|
+
|
|
97
|
+
export const VariantRounded = DisplayTemplate.bind({});
|
|
98
|
+
VariantRounded.args = { qrValue: "Rounded variant style", radius: 12, blackColor: "#059669" };
|
|
99
|
+
VariantRounded.parameters = { variant: { inner: "rounded", marker: "rounded", pixel: "rounded" } };
|
|
100
|
+
|
|
101
|
+
export const VariantDots = DisplayTemplate.bind({});
|
|
102
|
+
VariantDots.args = { qrValue: "Dots variant style", radius: 6, blackColor: "#dc2626" };
|
|
103
|
+
VariantDots.parameters = { variant: { inner: "dots", marker: "dots", pixel: "dots" } };
|
|
104
|
+
|
|
105
|
+
export const VariantMixed = DisplayTemplate.bind({});
|
|
106
|
+
VariantMixed.args = { qrValue: "Mixed variants for creative styling", radius: 10, blackColor: "#7c3aed" };
|
|
107
|
+
VariantMixed.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
108
|
+
|
|
109
|
+
export const WiFiConnection = DisplayTemplate.bind({});
|
|
110
|
+
WiFiConnection.args = { qrValue: "WIFI:T:WPA;S:MyHomeNetwork;P:mySecurePassword123;H:false;", blackColor: "#1e40af", radius: 4 };
|
|
111
|
+
WiFiConnection.parameters = { variant: { inner: "rounded", marker: "circle", pixel: "default" } };
|
|
112
|
+
|
|
113
|
+
export const VCardContact = DisplayTemplate.bind({});
|
|
114
|
+
VCardContact.args = {
|
|
115
|
+
qrValue: "BEGIN:VCARD\\nVERSION:3.0\\nFN:John Doe\\nORG:Acme Corp\\nTITLE:Software Engineer\\nTEL:+1234567890\\nEMAIL:john.doe@acme.com\\nURL:https://johndoe.dev\\nEND:VCARD",
|
|
116
|
+
blackColor: "#059669",
|
|
117
|
+
radius: 12,
|
|
118
|
+
};
|
|
119
|
+
VCardContact.parameters = { variant: { inner: "dots", marker: "rounded", pixel: "circle" } };
|
|
120
|
+
|
|
121
|
+
export const SmallSize = DisplayTemplate.bind({});
|
|
122
|
+
SmallSize.args = { qrValue: "Small QR for tight spaces", size: "128px", radius: 2, blackColor: "#f59e0b" };
|
|
123
|
+
SmallSize.parameters = { variant: { inner: "dots", marker: "circle", pixel: "rounded" } };
|
|
124
|
+
|
|
125
|
+
export const LargeSize = DisplayTemplate.bind({});
|
|
126
|
+
LargeSize.args = { qrValue: "Large QR for posters and signage", size: "400px", radius: 16, blackColor: "#0ea5e9" };
|
|
127
|
+
LargeSize.parameters = { variant: { inner: "circle", marker: "rounded", pixel: "dots" } };
|
|
128
|
+
|
|
129
|
+
export const HighContrast = DisplayTemplate.bind({});
|
|
130
|
+
HighContrast.args = { qrValue: "High contrast for accessibility", blackColor: "#000000", whiteColor: "#ffffff", radius: 0, size: "256px" };
|
|
131
|
+
HighContrast.parameters = { variant: { inner: "default", marker: "default", pixel: "default" } };
|
|
132
|
+
|
|
133
|
+
const PlaygroundTemplate: StoryFn<QrStoryArgs> = (args) => ({
|
|
134
|
+
components: { DisplayQrCodeComponent },
|
|
135
|
+
setup() {
|
|
136
|
+
const qrValue = ref(args.qrValue);
|
|
137
|
+
const variant = ref<QrCodeVariant>({ inner: "circle", marker: "rounded", pixel: "dots" });
|
|
138
|
+
const radius = ref(args.radius);
|
|
139
|
+
const blackColor = ref(args.blackColor);
|
|
140
|
+
const whiteColor = ref(args.whiteColor);
|
|
141
|
+
const sizeValue = ref(256);
|
|
142
|
+
const size = computed(() => `${sizeValue.value}px`);
|
|
143
|
+
const presetValues = [
|
|
144
|
+
{ label: "Website URL", value: "https://nuxt.com" },
|
|
145
|
+
{ label: "WiFi Network", value: "WIFI:T:WPA;S:MyNetwork;P:password123;" },
|
|
146
|
+
{ label: "Phone Number", value: "tel:+1234567890" },
|
|
147
|
+
{ label: "Email", value: "mailto:hello@example.com" },
|
|
148
|
+
{ label: "SMS", value: "sms:+1234567890?body=Hello!" },
|
|
149
|
+
{ label: "Location", value: "geo:40.7128,-74.0060" },
|
|
150
|
+
];
|
|
151
|
+
const variantOptions = ["default", "circle", "rounded", "dots"];
|
|
152
|
+
return { qrValue, variant, radius, blackColor, whiteColor, sizeValue, size, presetValues, variantOptions };
|
|
153
|
+
},
|
|
154
|
+
template: `
|
|
155
|
+
<div style="padding: 40px;">
|
|
156
|
+
<div style="display: grid; grid-template-columns: 1fr 300px; gap: 40px; align-items: start;">
|
|
157
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 400px; background: #f8fafc; border-radius: 8px; padding: 40px;">
|
|
158
|
+
<DisplayQrCodeComponent :qrValue="qrValue" :variant="variant" :radius="radius" :blackColor="blackColor" :whiteColor="whiteColor" :size="size" />
|
|
159
|
+
</div>
|
|
160
|
+
<div style="background: white; padding: 24px; border-radius: 8px; border: 1px solid #e5e7eb;">
|
|
161
|
+
<h3 style="margin: 0 0 20px 0; color: #374151; font-size: 18px;">QR Code Playground</h3>
|
|
162
|
+
<div style="margin-bottom: 20px;">
|
|
163
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Quick Presets:</label>
|
|
164
|
+
<select @change="qrValue = $event.target.value" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px;">
|
|
165
|
+
<option value="">Select a preset...</option>
|
|
166
|
+
<option v-for="preset in presetValues" :key="preset.label" :value="preset.value">{{ preset.label }}</option>
|
|
167
|
+
</select>
|
|
168
|
+
</div>
|
|
169
|
+
<div style="margin-bottom: 20px;">
|
|
170
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Custom Content:</label>
|
|
171
|
+
<textarea v-model="qrValue" rows="3" style="width: 100%; padding: 8px; border: 1px solid #d1d5db; border-radius: 4px; font-family: monospace; font-size: 12px;" placeholder="Enter QR code content..." />
|
|
172
|
+
</div>
|
|
173
|
+
<div style="margin-bottom: 20px;">
|
|
174
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Style Variants:</label>
|
|
175
|
+
<div style="display: grid; gap: 8px;">
|
|
176
|
+
<div><label style="font-size: 12px; color: #6b7280;">Inner:</label><select v-model="variant.inner" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
177
|
+
<div><label style="font-size: 12px; color: #6b7280;">Marker:</label><select v-model="variant.marker" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
178
|
+
<div><label style="font-size: 12px; color: #6b7280;">Pixel:</label><select v-model="variant.pixel" style="width: 100%; padding: 4px; border: 1px solid #d1d5db; border-radius: 4px; font-size: 12px;"><option v-for="option in variantOptions" :key="option" :value="option">{{ option }}</option></select></div>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<div style="margin-bottom: 20px;">
|
|
182
|
+
<label style="display: block; margin-bottom: 8px; font-weight: 500; color: #374151;">Appearance:</label>
|
|
183
|
+
<div style="display: grid; gap: 12px;">
|
|
184
|
+
<div><label style="font-size: 12px; color: #6b7280;">Size: {{ sizeValue }}px</label><input v-model.number="sizeValue" type="range" min="128" max="400" step="16" style="width: 100%;" /></div>
|
|
185
|
+
<div><label style="font-size: 12px; color: #6b7280;">Radius: {{ radius }}px</label><input v-model.number="radius" type="range" min="0" max="20" step="1" style="width: 100%;" /></div>
|
|
186
|
+
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px;">
|
|
187
|
+
<div><label style="font-size: 12px; color: #6b7280;">Foreground:</label><input v-model="blackColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
188
|
+
<div><label style="font-size: 12px; color: #6b7280;">Background:</label><input v-model="whiteColor" type="color" style="width: 100%; height: 32px; border: 1px solid #d1d5db; border-radius: 4px;" /></div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
</div>
|
|
194
|
+
</div>
|
|
195
|
+
`,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
export const InteractivePlayground = PlaygroundTemplate.bind({});
|
|
199
|
+
InteractivePlayground.args = {
|
|
200
|
+
qrValue: "https://github.com/srcdev/nuxt-components",
|
|
201
|
+
radius: 8,
|
|
202
|
+
blackColor: "#000000",
|
|
203
|
+
whiteColor: "transparent",
|
|
204
|
+
size: "256px",
|
|
205
|
+
styleClassPassthrough: [],
|
|
206
|
+
};
|