srcdev-nuxt-components 9.1.43 → 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/skills/components/display-avatar.md +187 -0
- package/.claude/skills/components/display-chip.md +213 -0
- package/.claude/skills/index.md +2 -0
- 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/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/ui/display-chip.vue +201 -66
- package/package.json +1 -1
- package/app/components/display-avatar/DisplayAvatar.vue +0 -148
- package/app/components/display-chip/DisplayChip.vue +0 -187
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { mountSuspended } from "@nuxt/test-utils/runtime";
|
|
3
|
+
import DisplayAvatar from "../DisplayAvatar.vue";
|
|
4
|
+
import DisplayChip from "../../../02.molecules/display-chip/DisplayChip.vue";
|
|
5
|
+
|
|
6
|
+
describe("DisplayAvatar", () => {
|
|
7
|
+
// ─── Mount ───────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
it("mounts without error", async () => {
|
|
10
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
11
|
+
expect(wrapper.vm).toBeTruthy();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// ─── Snapshots ───────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
it("renders correct HTML structure (default)", async () => {
|
|
17
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
18
|
+
props: { alt: "John Doe" },
|
|
19
|
+
});
|
|
20
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("renders correct HTML structure (with src)", async () => {
|
|
24
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
25
|
+
props: { src: "/avatar.jpg", alt: "John Doe", size: "lg" },
|
|
26
|
+
});
|
|
27
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("renders correct HTML structure (with chip)", async () => {
|
|
31
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
32
|
+
props: { alt: "John Doe", chip: true },
|
|
33
|
+
});
|
|
34
|
+
expect(wrapper.html()).toMatchSnapshot();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ─── Root element ─────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
it("renders as <span> by default", async () => {
|
|
40
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
41
|
+
expect(wrapper.element.tagName).toBe("SPAN");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("renders as the element specified by the as prop", async () => {
|
|
45
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
46
|
+
props: { as: "div" },
|
|
47
|
+
});
|
|
48
|
+
expect(wrapper.element.tagName).toBe("DIV");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders as DisplayChip when chip prop is set", async () => {
|
|
52
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
53
|
+
props: { chip: true },
|
|
54
|
+
});
|
|
55
|
+
expect(wrapper.findComponent(DisplayChip).exists()).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// ─── Base class ───────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
it("always has the display-avatar class", async () => {
|
|
61
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
62
|
+
expect(wrapper.classes()).toContain("display-avatar");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// ─── Size ─────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
it("applies md size class by default", async () => {
|
|
68
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
69
|
+
expect(wrapper.classes()).toContain("md");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it.each(["xs", "s", "md", "lg", "xl"] as const)("applies %s size class when size='%s'", async (size) => {
|
|
73
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { size } });
|
|
74
|
+
expect(wrapper.classes()).toContain(size);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ─── Fallback text ────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
it("shows initials derived from alt when no src or text is set", async () => {
|
|
80
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
81
|
+
props: { alt: "John Doe" },
|
|
82
|
+
});
|
|
83
|
+
expect(wrapper.find("span").text()).toBe("JD");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("caps initials at two characters", async () => {
|
|
87
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
88
|
+
props: { alt: "Alice Bob Charlie" },
|
|
89
|
+
});
|
|
90
|
+
expect(wrapper.find("span").text()).toBe("AB");
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("shows single initial for a single-word alt", async () => {
|
|
94
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
95
|
+
props: { alt: "Alice" },
|
|
96
|
+
});
|
|
97
|
+
expect(wrapper.find("span").text()).toBe("A");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("shows text prop instead of alt-derived initials when text is set", async () => {
|
|
101
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
102
|
+
props: { alt: "John Doe", text: "?" },
|
|
103
|
+
});
|
|
104
|
+
expect(wrapper.find("span").text()).toBe("?");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("shows empty fallback when neither src, text, nor alt are set", async () => {
|
|
108
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
109
|
+
expect(wrapper.find("span").text()).toBe("");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// ─── Image ────────────────────────────────────────────────────────────────
|
|
113
|
+
|
|
114
|
+
it("renders an image element when src is provided", async () => {
|
|
115
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
116
|
+
props: { src: "/avatar.jpg" },
|
|
117
|
+
});
|
|
118
|
+
expect(wrapper.find(".avatar-image").exists()).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does not render an image element when src is not provided", async () => {
|
|
122
|
+
const wrapper = await mountSuspended(DisplayAvatar);
|
|
123
|
+
expect(wrapper.find(".avatar-image").exists()).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("falls back to 'Avatar' as alt text when src is set but alt is not", async () => {
|
|
127
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
128
|
+
props: { src: "/avatar.jpg" },
|
|
129
|
+
});
|
|
130
|
+
expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Avatar");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("uses the alt prop as alt text on the image", async () => {
|
|
134
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
135
|
+
props: { src: "/avatar.jpg", alt: "Profile picture" },
|
|
136
|
+
});
|
|
137
|
+
expect(wrapper.find(".avatar-image").attributes("alt")).toBe("Profile picture");
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// ─── Slots ────────────────────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
it("renders default slot content instead of the fallback", async () => {
|
|
143
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
144
|
+
props: { alt: "John Doe" },
|
|
145
|
+
slots: { default: "<img class='custom-avatar' src='/x.jpg' alt='x' />" },
|
|
146
|
+
});
|
|
147
|
+
expect(wrapper.find(".custom-avatar").exists()).toBe(true);
|
|
148
|
+
expect(wrapper.text()).not.toContain("JD");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("renders icon slot content", async () => {
|
|
152
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
153
|
+
slots: { icon: "<span class='status-icon'>●</span>" },
|
|
154
|
+
});
|
|
155
|
+
expect(wrapper.find(".status-icon").exists()).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// ─── Chip ─────────────────────────────────────────────────────────────────
|
|
159
|
+
|
|
160
|
+
it("passes chipDefaultConfig when chip=true", async () => {
|
|
161
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
162
|
+
props: { chip: true },
|
|
163
|
+
});
|
|
164
|
+
const chip = wrapper.findComponent(DisplayChip);
|
|
165
|
+
expect(chip.props("config")).toMatchObject({
|
|
166
|
+
size: "12px",
|
|
167
|
+
maskWidth: "4px",
|
|
168
|
+
offset: "0px",
|
|
169
|
+
angle: "90deg",
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("passes custom config when chip is an object", async () => {
|
|
174
|
+
const chipConfig = { size: "16px", maskWidth: "2px", offset: "4px", angle: "45deg" };
|
|
175
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
176
|
+
props: { chip: chipConfig },
|
|
177
|
+
});
|
|
178
|
+
const chip = wrapper.findComponent(DisplayChip);
|
|
179
|
+
expect(chip.props("config")).toMatchObject(chipConfig);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
it("applies a single styleClassPassthrough string", async () => {
|
|
185
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
186
|
+
props: { styleClassPassthrough: "online" },
|
|
187
|
+
});
|
|
188
|
+
expect(wrapper.classes()).toContain("online");
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("applies multiple styleClassPassthrough classes from an array", async () => {
|
|
192
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
193
|
+
props: { styleClassPassthrough: ["online", "featured"] },
|
|
194
|
+
});
|
|
195
|
+
expect(wrapper.classes()).toContain("online");
|
|
196
|
+
expect(wrapper.classes()).toContain("featured");
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("updates classes when styleClassPassthrough prop changes", async () => {
|
|
200
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
201
|
+
props: { styleClassPassthrough: ["online"] },
|
|
202
|
+
});
|
|
203
|
+
expect(wrapper.classes()).toContain("online");
|
|
204
|
+
await wrapper.setProps({ styleClassPassthrough: ["offline"] });
|
|
205
|
+
expect(wrapper.classes()).not.toContain("online");
|
|
206
|
+
expect(wrapper.classes()).toContain("offline");
|
|
207
|
+
});
|
|
208
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
|
+
|
|
3
|
+
exports[`DisplayAvatar > renders correct HTML structure (default) 1`] = `"<span class="display-avatar md" style-class-passthrough=""><span>JD</span></span>"`;
|
|
4
|
+
|
|
5
|
+
exports[`DisplayAvatar > renders correct HTML structure (with chip) 1`] = `
|
|
6
|
+
"<span class="display-chip-core circle display-avatar md" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><span>JD</span>
|
|
7
|
+
<!--v-if-->
|
|
8
|
+
<!--v-if--></span>"
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
exports[`DisplayAvatar > renders correct HTML structure (with src) 1`] = `"<span class="display-avatar lg" style-class-passthrough=""><img data-nuxt-img="" srcset="/_ipx/_/avatar.jpg 1x, /_ipx/_/avatar.jpg 2x" alt="John Doe" class="avatar-image" src="/_ipx/_/avatar.jpg"></span>"`;
|
|
@@ -0,0 +1,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>
|
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
|
`,
|