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,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>"`;
|
|
@@ -5,26 +5,71 @@
|
|
|
5
5
|
<LayoutRow tag="div" variant="popout">
|
|
6
6
|
<h1 class="page-heading-2">Display Chip</h1>
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
8
|
+
<!-- ── QA Panel (dev only) ───────────────────────────────── -->
|
|
9
|
+
<div v-if="isDev" class="qa-panel">
|
|
10
|
+
<details class="qa-panel__details">
|
|
11
|
+
<summary class="qa-panel__summary">
|
|
12
|
+
<span class="qa-panel__title">QA — DisplayChip</span>
|
|
13
|
+
<code class="qa-panel__status">
|
|
14
|
+
size:{{ qaSize }}px · mask:{{ qaMaskWidth }}px · offset:{{ qaOffset }}px · angle:{{ qaAngle }}deg
|
|
15
|
+
</code>
|
|
16
|
+
</summary>
|
|
17
|
+
<div class="qa-panel__body">
|
|
18
|
+
|
|
19
|
+
<div class="qa-panel__group">
|
|
20
|
+
<span class="qa-panel__label">Size</span>
|
|
21
|
+
<div class="qa-panel__chips">
|
|
22
|
+
<button
|
|
23
|
+
v-for="preset in sizePresets"
|
|
24
|
+
:key="preset"
|
|
25
|
+
class="qa-panel__chip"
|
|
26
|
+
:class="{ 'is-active': qaSize === preset }"
|
|
27
|
+
@click="qaSize = preset"
|
|
28
|
+
>{{ preset }}px</button>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
|
|
32
|
+
<div class="qa-panel__group">
|
|
33
|
+
<span class="qa-panel__label">Mask Width</span>
|
|
34
|
+
<div class="qa-panel__chips">
|
|
35
|
+
<button
|
|
36
|
+
v-for="preset in maskWidthPresets"
|
|
37
|
+
:key="preset"
|
|
38
|
+
class="qa-panel__chip"
|
|
39
|
+
:class="{ 'is-active': qaMaskWidth === preset }"
|
|
40
|
+
@click="qaMaskWidth = preset"
|
|
41
|
+
>{{ preset }}px</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<div class="qa-panel__group">
|
|
46
|
+
<span class="qa-panel__label">Offset</span>
|
|
47
|
+
<div class="qa-panel__chips">
|
|
48
|
+
<button
|
|
49
|
+
v-for="preset in offsetPresets"
|
|
50
|
+
:key="preset"
|
|
51
|
+
class="qa-panel__chip"
|
|
52
|
+
:class="{ 'is-active': qaOffset === preset }"
|
|
53
|
+
@click="qaOffset = preset"
|
|
54
|
+
>{{ preset }}px</button>
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
|
|
58
|
+
<div class="qa-panel__group">
|
|
59
|
+
<span class="qa-panel__label">Angle — {{ qaAngle }}deg</span>
|
|
60
|
+
<input
|
|
61
|
+
v-model.number="qaAngle"
|
|
62
|
+
type="range"
|
|
63
|
+
min="0"
|
|
64
|
+
max="360"
|
|
65
|
+
step="1"
|
|
66
|
+
class="qa-panel__range"
|
|
67
|
+
/>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
25
70
|
</div>
|
|
26
|
-
</
|
|
27
|
-
</
|
|
71
|
+
</details>
|
|
72
|
+
</div>
|
|
28
73
|
|
|
29
74
|
<section>
|
|
30
75
|
<div class="dl">
|
|
@@ -106,6 +151,8 @@
|
|
|
106
151
|
</template>
|
|
107
152
|
|
|
108
153
|
<script setup lang="ts">
|
|
154
|
+
import type { DisplayChipConfig } from "~/types/components";
|
|
155
|
+
|
|
109
156
|
definePageMeta({
|
|
110
157
|
layout: false,
|
|
111
158
|
});
|
|
@@ -118,59 +165,148 @@ useHead({
|
|
|
118
165
|
},
|
|
119
166
|
});
|
|
120
167
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
168
|
+
// ── QA controls (dev only) ────────────────────────────────────────
|
|
169
|
+
const isDev = import.meta.dev;
|
|
170
|
+
|
|
171
|
+
const qaSize = ref(12);
|
|
172
|
+
const qaMaskWidth = ref(4);
|
|
173
|
+
const qaOffset = ref(2);
|
|
174
|
+
const qaAngle = ref(45);
|
|
127
175
|
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const changeOffset = (e: Event) => {
|
|
139
|
-
const target = e.target as HTMLInputElement;
|
|
140
|
-
chipConfig.offset = `${target.value}px`;
|
|
141
|
-
};
|
|
142
|
-
|
|
143
|
-
const changeAngle = (e: Event) => {
|
|
144
|
-
const target = e.target as HTMLInputElement;
|
|
145
|
-
chipConfig.angle = `${target.value}deg`;
|
|
146
|
-
};
|
|
176
|
+
const sizePresets = [8, 10, 12, 16, 20, 24];
|
|
177
|
+
const maskWidthPresets = [0, 2, 4, 6, 8];
|
|
178
|
+
const offsetPresets = [-4, 0, 2, 4, 8];
|
|
179
|
+
|
|
180
|
+
const chipConfig = computed((): DisplayChipConfig => ({
|
|
181
|
+
size: `${qaSize.value}px`,
|
|
182
|
+
maskWidth: `${qaMaskWidth.value}px`,
|
|
183
|
+
offset: `${qaOffset.value}px`,
|
|
184
|
+
angle: `${qaAngle.value}deg`,
|
|
185
|
+
}));
|
|
147
186
|
</script>
|
|
148
187
|
|
|
149
188
|
<style lang="css">
|
|
150
189
|
.ui-display-chip-page {
|
|
151
|
-
|
|
152
|
-
margin-bottom: 2rem;
|
|
153
|
-
|
|
154
|
-
.form-row {
|
|
155
|
-
.form-col {
|
|
156
|
-
display: grid;
|
|
157
|
-
grid-template-columns: 200px 200px;
|
|
158
|
-
gap: 1rem;
|
|
159
|
-
padding: 0.5rem 0;
|
|
160
|
-
|
|
161
|
-
label {
|
|
162
|
-
font-weight: bold;
|
|
163
|
-
}
|
|
190
|
+
/* ── QA Panel ──────────────────────────────────────────────────── */
|
|
164
191
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
192
|
+
.qa-panel {
|
|
193
|
+
background: oklch(15% 0 0);
|
|
194
|
+
color: white;
|
|
195
|
+
font-size: 1.3rem;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
.qa-panel__details {
|
|
199
|
+
padding: 1rem 2rem;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.qa-panel__summary {
|
|
203
|
+
cursor: pointer;
|
|
204
|
+
display: flex;
|
|
205
|
+
align-items: center;
|
|
206
|
+
gap: 1.6rem;
|
|
207
|
+
list-style: none;
|
|
208
|
+
user-select: none;
|
|
209
|
+
|
|
210
|
+
&::-webkit-details-marker { display: none; }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.qa-panel__title {
|
|
214
|
+
font-weight: 600;
|
|
215
|
+
font-size: 1.1rem;
|
|
216
|
+
text-transform: uppercase;
|
|
217
|
+
letter-spacing: 0.08em;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.qa-panel__status {
|
|
221
|
+
font-family: monospace;
|
|
222
|
+
font-size: 1.2rem;
|
|
223
|
+
background: oklch(0% 0 0 / 0.3);
|
|
224
|
+
padding: 0.2rem 0.8rem;
|
|
225
|
+
border-radius: 0.4rem;
|
|
226
|
+
user-select: text;
|
|
227
|
+
cursor: text;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
.qa-panel__body {
|
|
231
|
+
display: flex;
|
|
232
|
+
flex-wrap: wrap;
|
|
233
|
+
gap: 2.4rem;
|
|
234
|
+
padding-block: 1.2rem 0.4rem;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
.qa-panel__group {
|
|
238
|
+
display: flex;
|
|
239
|
+
flex-direction: column;
|
|
240
|
+
gap: 0.6rem;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.qa-panel__label {
|
|
244
|
+
font-size: 1.1rem;
|
|
245
|
+
text-transform: uppercase;
|
|
246
|
+
letter-spacing: 0.08em;
|
|
247
|
+
opacity: 0.55;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.qa-panel__chips {
|
|
251
|
+
display: flex;
|
|
252
|
+
flex-wrap: wrap;
|
|
253
|
+
gap: 0.4rem;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.qa-panel__chip {
|
|
257
|
+
font-family: monospace;
|
|
258
|
+
font-size: 1.2rem;
|
|
259
|
+
color: white;
|
|
260
|
+
background: oklch(0% 0 0 / 0.25);
|
|
261
|
+
border: 1px solid oklch(100% 0 0 / 0.18);
|
|
262
|
+
padding: 0.3rem 1rem;
|
|
263
|
+
border-radius: 0.4rem;
|
|
264
|
+
cursor: pointer;
|
|
265
|
+
transition: background 0.15s;
|
|
266
|
+
|
|
267
|
+
&:hover { background: oklch(0% 0 0 / 0.4); }
|
|
268
|
+
|
|
269
|
+
&.is-active {
|
|
270
|
+
background: oklch(55% 0.18 240);
|
|
271
|
+
border-color: oklch(55% 0.18 240);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.qa-panel__range {
|
|
276
|
+
appearance: none;
|
|
277
|
+
width: 18rem;
|
|
278
|
+
height: 0.4rem;
|
|
279
|
+
background: oklch(100% 0 0 / 0.18);
|
|
280
|
+
border-radius: 0.4rem;
|
|
281
|
+
outline: none;
|
|
282
|
+
cursor: pointer;
|
|
283
|
+
|
|
284
|
+
&::-webkit-slider-thumb {
|
|
285
|
+
appearance: none;
|
|
286
|
+
width: 1.4rem;
|
|
287
|
+
height: 1.4rem;
|
|
288
|
+
border-radius: 50%;
|
|
289
|
+
background: oklch(55% 0.18 240);
|
|
290
|
+
cursor: pointer;
|
|
291
|
+
transition: background 0.15s;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
&::-moz-range-thumb {
|
|
295
|
+
width: 1.4rem;
|
|
296
|
+
height: 1.4rem;
|
|
297
|
+
border: none;
|
|
298
|
+
border-radius: 50%;
|
|
299
|
+
background: oklch(55% 0.18 240);
|
|
300
|
+
cursor: pointer;
|
|
301
|
+
transition: background 0.15s;
|
|
172
302
|
}
|
|
303
|
+
|
|
304
|
+
&:hover::-webkit-slider-thumb { background: oklch(62% 0.18 240); }
|
|
305
|
+
&:hover::-moz-range-thumb { background: oklch(62% 0.18 240); }
|
|
173
306
|
}
|
|
307
|
+
|
|
308
|
+
/* ── Demo grid ─────────────────────────────────────────────────── */
|
|
309
|
+
|
|
174
310
|
section {
|
|
175
311
|
margin-top: 2rem;
|
|
176
312
|
|
|
@@ -181,11 +317,10 @@ const changeAngle = (e: Event) => {
|
|
|
181
317
|
align-items: center;
|
|
182
318
|
justify-content: start;
|
|
183
319
|
|
|
184
|
-
/* background-color: var(--slate-05); */
|
|
185
|
-
|
|
186
320
|
.dt {
|
|
187
321
|
font-weight: bold;
|
|
188
322
|
}
|
|
323
|
+
|
|
189
324
|
.dd {
|
|
190
325
|
margin: 0;
|
|
191
326
|
|
package/package.json
CHANGED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<component
|
|
3
|
-
:is="props.chip ? DisplayChip : as"
|
|
4
|
-
v-bind="props.chip ? (typeof props.chip === 'object' ? { config: props.chip } : { config: chipDefaultConfig }) : {}"
|
|
5
|
-
class="display-avatar"
|
|
6
|
-
:class="[size, elementClasses]"
|
|
7
|
-
:style-class-passthrough="elementClasses"
|
|
8
|
-
>
|
|
9
|
-
<slot name="default">
|
|
10
|
-
<NuxtImg v-if="src" :src :alt="alt || 'Avatar'" width="100%" height="100%" class="avatar-image" />
|
|
11
|
-
<span v-else>{{ fallback }}</span>
|
|
12
|
-
</slot>
|
|
13
|
-
<slot name="icon"></slot>
|
|
14
|
-
</component>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import DisplayChip from "../display-chip/DisplayChip.vue";
|
|
19
|
-
import type { DisplayChipProps } from "../../types/components";
|
|
20
|
-
|
|
21
|
-
export interface AvatarSlots {
|
|
22
|
-
default(props?: {}): any;
|
|
23
|
-
icon(props?: {}): any;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const props = defineProps({
|
|
27
|
-
as: {
|
|
28
|
-
type: [String, Object] as PropType<any>,
|
|
29
|
-
default: "span",
|
|
30
|
-
},
|
|
31
|
-
src: {
|
|
32
|
-
type: String,
|
|
33
|
-
default: undefined,
|
|
34
|
-
},
|
|
35
|
-
alt: {
|
|
36
|
-
type: String,
|
|
37
|
-
default: undefined,
|
|
38
|
-
},
|
|
39
|
-
text: {
|
|
40
|
-
type: String,
|
|
41
|
-
default: undefined,
|
|
42
|
-
},
|
|
43
|
-
size: {
|
|
44
|
-
type: String as PropType<"xs" | "s" | "md" | "lg" | "xl" | string>,
|
|
45
|
-
default: "md",
|
|
46
|
-
},
|
|
47
|
-
chip: {
|
|
48
|
-
type: [Boolean, Object] as PropType<boolean | DisplayChipProps>,
|
|
49
|
-
default: undefined,
|
|
50
|
-
},
|
|
51
|
-
class: {
|
|
52
|
-
type: [String, Array, Object] as PropType<any>,
|
|
53
|
-
default: undefined,
|
|
54
|
-
},
|
|
55
|
-
style: {
|
|
56
|
-
type: [String, Array, Object] as PropType<any>,
|
|
57
|
-
default: undefined,
|
|
58
|
-
},
|
|
59
|
-
styleClassPassthrough: {
|
|
60
|
-
type: [String, Array] as PropType<string | string[]>,
|
|
61
|
-
default: () => [],
|
|
62
|
-
},
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
defineSlots<AvatarSlots>();
|
|
66
|
-
|
|
67
|
-
const { elementClasses, resetElementClasses, updateElementClasses } = useStyleClassPassthrough(
|
|
68
|
-
props.styleClassPassthrough
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
if (props.chip && typeof props.chip === "object" && !("styleClassPassthrough" in props.chip)) {
|
|
72
|
-
updateElementClasses(["display-avatar", props.size]);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
const fallback = computed(
|
|
76
|
-
() =>
|
|
77
|
-
props.text ||
|
|
78
|
-
(props.alt || "")
|
|
79
|
-
.split(" ")
|
|
80
|
-
.map((word) => word.charAt(0))
|
|
81
|
-
.join("")
|
|
82
|
-
.substring(0, 2)
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
const chipDefaultConfig = {
|
|
86
|
-
size: "12px",
|
|
87
|
-
maskWidth: "4px",
|
|
88
|
-
offset: "0px",
|
|
89
|
-
angle: "90deg",
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
watch(
|
|
93
|
-
() => props.styleClassPassthrough,
|
|
94
|
-
() => {
|
|
95
|
-
resetElementClasses(props.styleClassPassthrough);
|
|
96
|
-
}
|
|
97
|
-
);
|
|
98
|
-
</script>
|
|
99
|
-
|
|
100
|
-
<style lang="css">
|
|
101
|
-
@layer components {
|
|
102
|
-
.display-avatar {
|
|
103
|
-
display: flex;
|
|
104
|
-
align-items: center;
|
|
105
|
-
justify-content: center;
|
|
106
|
-
border-radius: 50%;
|
|
107
|
-
color: var(--slate-03);
|
|
108
|
-
|
|
109
|
-
isolation: isolate;
|
|
110
|
-
|
|
111
|
-
&.xs {
|
|
112
|
-
width: 24px;
|
|
113
|
-
height: 24px;
|
|
114
|
-
font-size: 0.75rem;
|
|
115
|
-
}
|
|
116
|
-
&.s {
|
|
117
|
-
width: 32px;
|
|
118
|
-
height: 32px;
|
|
119
|
-
font-size: 0.875rem;
|
|
120
|
-
}
|
|
121
|
-
&.md {
|
|
122
|
-
width: 40px;
|
|
123
|
-
height: 40px;
|
|
124
|
-
font-size: 1rem;
|
|
125
|
-
}
|
|
126
|
-
&.lg {
|
|
127
|
-
width: 48px;
|
|
128
|
-
height: 48px;
|
|
129
|
-
font-size: 1.125rem;
|
|
130
|
-
}
|
|
131
|
-
&.xl {
|
|
132
|
-
width: 56px;
|
|
133
|
-
height: 56px;
|
|
134
|
-
font-size: 1.25rem;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.avatar-image {
|
|
138
|
-
width: 100%;
|
|
139
|
-
border-radius: 50%;
|
|
140
|
-
object-fit: cover;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
.avatar-icon {
|
|
144
|
-
font-size: 24px;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
</style>
|