srcdev-nuxt-components 9.1.43 → 9.1.45

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.
Files changed (22) hide show
  1. package/.claude/skills/components/display-avatar.md +187 -0
  2. package/.claude/skills/components/display-chip.md +213 -0
  3. package/.claude/skills/components/display-pill.md +162 -0
  4. package/.claude/skills/index.md +3 -0
  5. package/app/components/01.atoms/display-avatar/DisplayAvatar.vue +130 -0
  6. package/app/components/{display-avatar → 01.atoms/display-avatar}/stories/DisplayAvatar.stories.ts +13 -1
  7. package/app/components/01.atoms/display-avatar/tests/DisplayAvatar.spec.ts +208 -0
  8. package/app/components/01.atoms/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +11 -0
  9. package/app/components/01.atoms/display-pill/DisplayPill.vue +134 -0
  10. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +88 -0
  11. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +159 -0
  12. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +7 -0
  13. package/app/components/02.molecules/display-chip/DisplayChip.vue +189 -0
  14. package/app/components/{display-chip → 02.molecules/display-chip}/stories/DisplayChip.stories.ts +38 -54
  15. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +191 -0
  16. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +12 -0
  17. package/app/layouts/default.vue +1 -0
  18. package/app/pages/ui/display-chip.vue +201 -66
  19. package/app/pages/ui/display-pill.vue +402 -0
  20. package/package.json +1 -1
  21. package/app/components/display-avatar/DisplayAvatar.vue +0 -148
  22. package/app/components/display-chip/DisplayChip.vue +0 -187
@@ -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>
@@ -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 "../../../types/components";
4
+ import type { DisplayChipConfig } from "~/types/components";
4
5
 
5
6
  // Custom interface for story args
6
7
  interface ChipStoryArgs {
@@ -19,7 +20,7 @@ interface ChipStoryArgs {
19
20
  }
20
21
 
21
22
  export default {
22
- title: "Components/UI/DisplayChip",
23
+ title: "Molecules/DisplayChip",
23
24
  component: StorybookComponent,
24
25
  argTypes: {
25
26
  // Basic Configuration
@@ -129,7 +130,7 @@ export default {
129
130
  label: "",
130
131
  status: "offline",
131
132
  useSlot: true,
132
- slotContent: "Avatar content",
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: DisplayChipConfig = {
141
- size: `${args.chipSize}px`,
142
- maskWidth: `${args.chipMaskWidth}px`,
143
- offset: `${args.chipOffset}px`,
144
- angle: `${args.chipAngle}deg`,
145
- icon: args.icon || undefined,
146
- label: args.label || undefined,
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="padding: 40px; display: flex; align-items: center; justify-content: center; min-height: 200px;">
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
- {{ args.slotContent }}
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="padding: 40px; display: flex; gap: 40px; align-items: center; justify-content: center; flex-wrap: wrap;">
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
- Online
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
- Idle
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
- DND
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
- Offline
317
+ <div style="width: 50px; height: 50px; background: #64748b; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: #f8fafc; font-weight: 600; font-size: 1.3rem; font-family: sans-serif;">SRC</div>
334
318
  </StorybookComponent>
335
319
  </div>
336
320
  `,
@@ -0,0 +1,191 @@
1
+ import { describe, it, expect, vi } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayChip from "../DisplayChip.vue";
4
+
5
+ describe("DisplayChip", () => {
6
+ // ─── Mount ───────────────────────────────────────────────────────────────
7
+
8
+ it("mounts without error", async () => {
9
+ const wrapper = await mountSuspended(DisplayChip);
10
+ expect(wrapper.vm).toBeTruthy();
11
+ });
12
+
13
+ // ─── Snapshots ───────────────────────────────────────────────────────────
14
+
15
+ it("renders correct HTML structure (default)", async () => {
16
+ const wrapper = await mountSuspended(DisplayChip);
17
+ expect(wrapper.html()).toMatchSnapshot();
18
+ });
19
+
20
+ it("renders correct HTML structure (with label)", async () => {
21
+ const wrapper = await mountSuspended(DisplayChip, {
22
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "5" } },
23
+ });
24
+ expect(wrapper.html()).toMatchSnapshot();
25
+ });
26
+
27
+ it("renders correct HTML structure (with icon)", async () => {
28
+ const wrapper = await mountSuspended(DisplayChip, {
29
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", icon: "mdi:check" } },
30
+ });
31
+ expect(wrapper.html()).toMatchSnapshot();
32
+ });
33
+
34
+ it("renders correct HTML structure (square + styleClassPassthrough)", async () => {
35
+ const wrapper = await mountSuspended(DisplayChip, {
36
+ props: { shape: "square", styleClassPassthrough: ["online"] },
37
+ });
38
+ expect(wrapper.html()).toMatchSnapshot();
39
+ });
40
+
41
+ // ─── Root element ─────────────────────────────────────────────────────────
42
+
43
+ it("renders as <span> by default", async () => {
44
+ const wrapper = await mountSuspended(DisplayChip);
45
+ expect(wrapper.element.tagName).toBe("SPAN");
46
+ });
47
+
48
+ it("renders as <div> when tag='div'", async () => {
49
+ const wrapper = await mountSuspended(DisplayChip, { props: { tag: "div" } });
50
+ expect(wrapper.element.tagName).toBe("DIV");
51
+ });
52
+
53
+ // ─── Base class ───────────────────────────────────────────────────────────
54
+
55
+ it("always has the display-chip-core class", async () => {
56
+ const wrapper = await mountSuspended(DisplayChip);
57
+ expect(wrapper.classes()).toContain("display-chip-core");
58
+ });
59
+
60
+ // ─── Shape ────────────────────────────────────────────────────────────────
61
+
62
+ it("applies circle class by default", async () => {
63
+ const wrapper = await mountSuspended(DisplayChip);
64
+ expect(wrapper.classes()).toContain("circle");
65
+ });
66
+
67
+ it("applies square class when shape='square'", async () => {
68
+ const wrapper = await mountSuspended(DisplayChip, { props: { shape: "square" } });
69
+ expect(wrapper.classes()).toContain("square");
70
+ expect(wrapper.classes()).not.toContain("circle");
71
+ });
72
+
73
+ // ─── CSS custom properties ────────────────────────────────────────────────
74
+
75
+ it("sets CSS custom properties from config", async () => {
76
+ const wrapper = await mountSuspended(DisplayChip, {
77
+ props: {
78
+ config: { size: "16px", maskWidth: "3px", offset: "4px", angle: "45deg" },
79
+ },
80
+ });
81
+ const style = (wrapper.element as HTMLElement).style;
82
+ expect(style.getPropertyValue("--chip-size")).toBe("16px");
83
+ expect(style.getPropertyValue("--chip-mask-width")).toBe("3px");
84
+ expect(style.getPropertyValue("--chip-offset")).toBe("4px");
85
+ expect(style.getPropertyValue("--chip-angle")).toBe("45deg");
86
+ });
87
+
88
+ it("sets default CSS custom properties when no config is provided", async () => {
89
+ const wrapper = await mountSuspended(DisplayChip);
90
+ const style = (wrapper.element as HTMLElement).style;
91
+ expect(style.getPropertyValue("--chip-size")).toBe("12px");
92
+ expect(style.getPropertyValue("--chip-mask-width")).toBe("4px");
93
+ expect(style.getPropertyValue("--chip-offset")).toBe("0px");
94
+ expect(style.getPropertyValue("--chip-angle")).toBe("90deg");
95
+ });
96
+
97
+ // ─── Label ────────────────────────────────────────────────────────────────
98
+
99
+ it("does not render .chip-label when config has no label", async () => {
100
+ const wrapper = await mountSuspended(DisplayChip);
101
+ expect(wrapper.find(".chip-label").exists()).toBe(false);
102
+ });
103
+
104
+ it("renders .chip-label when config.label is set", async () => {
105
+ const wrapper = await mountSuspended(DisplayChip, {
106
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "5" } },
107
+ });
108
+ expect(wrapper.find(".chip-label").exists()).toBe(true);
109
+ expect(wrapper.find(".chip-label").text()).toBe("5");
110
+ });
111
+
112
+ it.each([
113
+ ["A", "length-1"],
114
+ ["+2", "length-2"],
115
+ ["DND", "length-3"],
116
+ ])("applies length class for label '%s'", async (label, expectedClass) => {
117
+ const wrapper = await mountSuspended(DisplayChip, {
118
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label } },
119
+ });
120
+ expect(wrapper.find(".chip-label").classes()).toContain(expectedClass);
121
+ });
122
+
123
+ it("truncates label to 3 characters when label exceeds maximum length", async () => {
124
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
125
+ const wrapper = await mountSuspended(DisplayChip, {
126
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "ABCD" } },
127
+ });
128
+ expect(wrapper.find(".chip-label").text()).toBe("ABC");
129
+ warnSpy.mockRestore();
130
+ });
131
+
132
+ it("emits a console warning when label exceeds 3 characters", async () => {
133
+ const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
134
+ await mountSuspended(DisplayChip, {
135
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", label: "ABCD" } },
136
+ });
137
+ expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining("exceeds maximum length of 3 characters"));
138
+ warnSpy.mockRestore();
139
+ });
140
+
141
+ // ─── Icon ─────────────────────────────────────────────────────────────────
142
+
143
+ it("does not render .chip-icon when config has no icon", async () => {
144
+ const wrapper = await mountSuspended(DisplayChip);
145
+ expect(wrapper.find(".chip-icon").exists()).toBe(false);
146
+ });
147
+
148
+ it("renders .chip-icon when config.icon is set", async () => {
149
+ const wrapper = await mountSuspended(DisplayChip, {
150
+ props: { config: { size: "12px", maskWidth: "4px", offset: "0px", angle: "90deg", icon: "mdi:check" } },
151
+ });
152
+ expect(wrapper.find(".chip-icon").exists()).toBe(true);
153
+ });
154
+
155
+ // ─── Slot ─────────────────────────────────────────────────────────────────
156
+
157
+ it("renders default slot content", async () => {
158
+ const wrapper = await mountSuspended(DisplayChip, {
159
+ slots: { default: "<div class='avatar'>SRC</div>" },
160
+ });
161
+ expect(wrapper.find(".avatar").exists()).toBe(true);
162
+ expect(wrapper.find(".avatar").text()).toBe("SRC");
163
+ });
164
+
165
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
166
+
167
+ it("applies a single styleClassPassthrough string", async () => {
168
+ const wrapper = await mountSuspended(DisplayChip, {
169
+ props: { styleClassPassthrough: "online" },
170
+ });
171
+ expect(wrapper.classes()).toContain("online");
172
+ });
173
+
174
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
175
+ const wrapper = await mountSuspended(DisplayChip, {
176
+ props: { styleClassPassthrough: ["online", "featured"] },
177
+ });
178
+ expect(wrapper.classes()).toContain("online");
179
+ expect(wrapper.classes()).toContain("featured");
180
+ });
181
+
182
+ it("updates classes when styleClassPassthrough prop changes", async () => {
183
+ const wrapper = await mountSuspended(DisplayChip, {
184
+ props: { styleClassPassthrough: ["online"] },
185
+ });
186
+ expect(wrapper.classes()).toContain("online");
187
+ await wrapper.setProps({ styleClassPassthrough: ["idle"] });
188
+ expect(wrapper.classes()).not.toContain("online");
189
+ expect(wrapper.classes()).toContain("idle");
190
+ });
191
+ });
@@ -0,0 +1,12 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayChip > renders correct HTML structure (default) 1`] = `"<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><!--v-if--></span>"`;
4
+
5
+ exports[`DisplayChip > renders correct HTML structure (square + styleClassPassthrough) 1`] = `"<span class="display-chip-core square online" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><!--v-if--></span>"`;
6
+
7
+ exports[`DisplayChip > renders correct HTML structure (with icon) 1`] = `
8
+ "<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><span class="iconify i-mdi:check chip-icon" aria-hidden="true"></span>
9
+ <!--v-if--></span>"
10
+ `;
11
+
12
+ exports[`DisplayChip > renders correct HTML structure (with label) 1`] = `"<span class="display-chip-core circle" style="--chip-size: 12px; --chip-mask-width: 4px; --chip-offset: 0px; --chip-angle: 90deg;"><!--v-if--><span class="chip-label length-1">5</span></span>"`;
@@ -89,6 +89,7 @@ const responsiveNavLinks = {
89
89
  { name: "Clipped Panels", path: "/ui/clipped-panels" },
90
90
  { name: "Display Chip", path: "/ui/display-chip" },
91
91
  { name: "Display Avatar", path: "/ui/display-avatar" },
92
+ { name: "Display Pill", path: "/ui/display-pill" },
92
93
  { name: "Qr Codes", path: "/ui/qr-code/display" },
93
94
  ],
94
95
  },