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
@@ -2,9 +2,17 @@ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
2
  import StorybookComponent from "../DisplayAvatar.vue";
3
3
 
4
4
  export default {
5
- title: "Components/UI/DisplayAvatar",
5
+ title: "Atoms/DisplayAvatar",
6
6
  component: StorybookComponent,
7
7
  argTypes: {
8
+ size: {
9
+ control: { type: "inline-radio" },
10
+ options: ["xs", "s", "md", "lg", "xl"],
11
+ description: "Avatar size",
12
+ table: {
13
+ category: "Avatar",
14
+ },
15
+ },
8
16
  src: {
9
17
  control: { type: "text" },
10
18
  description: "Avatar image source URL",
@@ -61,6 +69,7 @@ export default {
61
69
  },
62
70
  },
63
71
  args: {
72
+ size: "md",
64
73
  src: "https://github.com/srcdev.png",
65
74
  alt: "SrcDev Avatar",
66
75
  chipSize: 12,
@@ -77,7 +86,9 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
77
86
  return { args };
78
87
  },
79
88
  template: `
89
+ <div style="display: flex; align-items: center; justify-content: center; height: 100vh;">
80
90
  <StorybookComponent
91
+ :size="args.size"
81
92
  :src="args.src"
82
93
  :alt="args.alt"
83
94
  :chip="{
@@ -88,6 +99,7 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
88
99
  }"
89
100
  :style-class-passthrough="args.styleClassPassthrough"
90
101
  />
102
+ </div>
91
103
  `,
92
104
  });
93
105
 
@@ -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,134 @@
1
+ <template>
2
+ <component
3
+ :is="tag"
4
+ class="display-pill"
5
+ :class="[size, variant, { 'is-reversed': reversed }, elementClasses]"
6
+ >
7
+ <slot name="icon"></slot>
8
+ <span v-if="label" class="pill-label">{{ label }}</span>
9
+ <slot v-else name="default"></slot>
10
+ </component>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ interface Props {
15
+ tag?: "span" | "div" | "button" | "a";
16
+ label?: string;
17
+ size?: "sm" | "md" | "lg";
18
+ variant?: "default" | "primary" | "success" | "warning" | "danger" | "neutral";
19
+ reversed?: boolean;
20
+ styleClassPassthrough?: string | string[];
21
+ }
22
+
23
+ const props = withDefaults(defineProps<Props>(), {
24
+ tag: "span",
25
+ label: undefined,
26
+ size: "md",
27
+ variant: "default",
28
+ reversed: false,
29
+ styleClassPassthrough: () => [],
30
+ });
31
+
32
+ const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
33
+
34
+ watch(
35
+ () => props.styleClassPassthrough,
36
+ () => {
37
+ resetElementClasses(props.styleClassPassthrough);
38
+ }
39
+ );
40
+ </script>
41
+
42
+ <style lang="css">
43
+ @layer components {
44
+ .display-pill {
45
+ --_bg: var(--theme-pill-bg, var(--slate-01));
46
+ --_color: var(--theme-pill-color, var(--slate-09));
47
+ --_border-color: var(--theme-pill-border-color, transparent);
48
+ --_border-width: var(--theme-pill-border-width, 1px);
49
+ --_border-style: var(--theme-pill-border-style, solid);
50
+ /* 100vw always resolves to the correct pill radius regardless of element size */
51
+ --_border-radius: var(--theme-pill-border-radius, 100vw);
52
+ --_outline: var(--theme-pill-outline, none);
53
+ --_outline-offset: var(--theme-pill-outline-offset, 0px);
54
+ --_gap: var(--theme-pill-gap, 0.5rem);
55
+ --_font-size: var(--theme-pill-font-size, 1.2rem);
56
+ --_font-weight: var(--theme-pill-font-weight, 500);
57
+ --_padding-x: var(--theme-pill-padding-x, 1rem);
58
+ --_padding-y: var(--theme-pill-padding-y, 0.4rem);
59
+ --_icon-size: var(--theme-pill-icon-size, 1.4rem);
60
+
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: var(--_gap);
64
+ padding: var(--_padding-y) var(--_padding-x);
65
+ border-radius: var(--_border-radius);
66
+ border: var(--_border-width) var(--_border-style) var(--_border-color);
67
+ outline: var(--_outline);
68
+ outline-offset: var(--_outline-offset);
69
+ background-color: var(--_bg);
70
+ color: var(--_color);
71
+ font-size: var(--_font-size);
72
+ font-weight: var(--_font-weight);
73
+ line-height: 1;
74
+ white-space: nowrap;
75
+ width: fit-content;
76
+ cursor: default;
77
+ user-select: none;
78
+
79
+ &:is(button, a) {
80
+ cursor: pointer;
81
+ }
82
+
83
+ &.is-reversed {
84
+ flex-direction: row-reverse;
85
+ }
86
+
87
+ .pill-label {
88
+ display: inline-flex;
89
+ align-items: center;
90
+ }
91
+
92
+ /* Sizes */
93
+ &.sm {
94
+ --_font-size: var(--theme-pill-font-size-sm, 1rem);
95
+ --_padding-x: var(--theme-pill-padding-x-sm, 0.8rem);
96
+ --_padding-y: var(--theme-pill-padding-y-sm, 0.3rem);
97
+ --_icon-size: var(--theme-pill-icon-size-sm, 1.2rem);
98
+ }
99
+
100
+ &.lg {
101
+ --_font-size: var(--theme-pill-font-size-lg, 1.4rem);
102
+ --_padding-x: var(--theme-pill-padding-x-lg, 1.2rem);
103
+ --_padding-y: var(--theme-pill-padding-y-lg, 0.6rem);
104
+ --_icon-size: var(--theme-pill-icon-size-lg, 1.6rem);
105
+ }
106
+
107
+ /* Variants */
108
+ &.primary {
109
+ --_bg: var(--theme-pill-primary-bg, var(--blue-01));
110
+ --_color: var(--theme-pill-primary-color, var(--blue-09));
111
+ }
112
+
113
+ &.success {
114
+ --_bg: var(--theme-pill-success-bg, var(--green-01));
115
+ --_color: var(--theme-pill-success-color, var(--green-10));
116
+ }
117
+
118
+ &.warning {
119
+ --_bg: var(--theme-pill-warning-bg, var(--yellow-08, #fef9c3));
120
+ --_color: var(--theme-pill-warning-color, var(--yellow-03, #a16207));
121
+ }
122
+
123
+ &.danger {
124
+ --_bg: var(--theme-pill-danger-bg, var(--red-01));
125
+ --_color: var(--theme-pill-danger-color, var(--red-10));
126
+ }
127
+
128
+ &.neutral {
129
+ --_bg: var(--theme-pill-neutral-bg, var(--slate-08));
130
+ --_color: var(--theme-pill-neutral-color, var(--slate-03));
131
+ }
132
+ }
133
+ }
134
+ </style>
@@ -0,0 +1,88 @@
1
+ import type { Meta, StoryFn } from "@nuxtjs/storybook";
2
+ import StorybookComponent from "../DisplayPill.vue";
3
+
4
+ type StoryArgs = {
5
+ tag: "span" | "div" | "button" | "a";
6
+ label: string;
7
+ size: "sm" | "md" | "lg";
8
+ variant: "default" | "primary" | "success" | "warning" | "danger" | "neutral";
9
+ reversed: boolean;
10
+ showIcon: boolean;
11
+ };
12
+
13
+ export default {
14
+ title: "Atoms/DisplayPill",
15
+ component: StorybookComponent,
16
+ argTypes: {
17
+ tag: {
18
+ control: { type: "inline-radio" },
19
+ options: ["span", "div", "button", "a"],
20
+ description: "Root element tag",
21
+ },
22
+ label: {
23
+ control: { type: "text" },
24
+ description: "Pill label text",
25
+ },
26
+ size: {
27
+ control: { type: "inline-radio" },
28
+ options: ["sm", "md", "lg"],
29
+ description: "Pill size",
30
+ },
31
+ variant: {
32
+ control: { type: "inline-radio" },
33
+ options: ["default", "primary", "success", "warning", "danger", "neutral"],
34
+ description: "Colour variant",
35
+ },
36
+ reversed: {
37
+ control: { type: "boolean" },
38
+ description: "Swap icon and label order",
39
+ },
40
+ showIcon: {
41
+ control: { type: "boolean" },
42
+ description: "Show icon slot",
43
+ },
44
+ styleClassPassthrough: {
45
+ table: { disable: true },
46
+ },
47
+ class: {
48
+ table: { disable: true },
49
+ },
50
+ style: {
51
+ table: { disable: true },
52
+ },
53
+ },
54
+ args: {
55
+ tag: "span",
56
+ label: "Status",
57
+ size: "md",
58
+ variant: "default",
59
+ reversed: false,
60
+ showIcon: true,
61
+ },
62
+ } as Meta<StoryArgs>;
63
+
64
+ const Template: StoryFn<StoryArgs> = (args) => ({
65
+ components: { StorybookComponent },
66
+ setup() {
67
+ return { args };
68
+ },
69
+ template: `
70
+ <div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
71
+ <StorybookComponent
72
+ :tag="args.tag"
73
+ :label="args.label"
74
+ :size="args.size"
75
+ :variant="args.variant"
76
+ :reversed="args.reversed"
77
+ >
78
+ <template v-if="args.showIcon" #icon>
79
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
80
+ <circle cx="12" cy="12" r="5" />
81
+ </svg>
82
+ </template>
83
+ </StorybookComponent>
84
+ </div>
85
+ `,
86
+ });
87
+
88
+ export const Default = Template.bind({});
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mountSuspended } from "@nuxt/test-utils/runtime";
3
+ import DisplayPill from "../DisplayPill.vue";
4
+
5
+ describe("DisplayPill", () => {
6
+ // ─── Mount ───────────────────────────────────────────────────────────────
7
+
8
+ it("mounts without error", async () => {
9
+ const wrapper = await mountSuspended(DisplayPill);
10
+ expect(wrapper.vm).toBeTruthy();
11
+ });
12
+
13
+ // ─── Snapshots ───────────────────────────────────────────────────────────
14
+
15
+ it("renders correct HTML structure (default)", async () => {
16
+ const wrapper = await mountSuspended(DisplayPill, {
17
+ props: { label: "Status" },
18
+ });
19
+ expect(wrapper.html()).toMatchSnapshot();
20
+ });
21
+
22
+ it("renders correct HTML structure (with icon slot)", async () => {
23
+ const wrapper = await mountSuspended(DisplayPill, {
24
+ props: { label: "Status", variant: "success", size: "lg" },
25
+ slots: { icon: "<span class='icon'>✓</span>" },
26
+ });
27
+ expect(wrapper.html()).toMatchSnapshot();
28
+ });
29
+
30
+ it("renders correct HTML structure (reversed)", async () => {
31
+ const wrapper = await mountSuspended(DisplayPill, {
32
+ props: { label: "Status", reversed: true },
33
+ slots: { icon: "<span class='icon'>✓</span>" },
34
+ });
35
+ expect(wrapper.html()).toMatchSnapshot();
36
+ });
37
+
38
+ // ─── Root element ─────────────────────────────────────────────────────────
39
+
40
+ it("renders as <span> by default", async () => {
41
+ const wrapper = await mountSuspended(DisplayPill);
42
+ expect(wrapper.element.tagName).toBe("SPAN");
43
+ });
44
+
45
+ it.each(["span", "div", "button", "a"] as const)("renders as <%s> when tag='%s'", async (tag) => {
46
+ const wrapper = await mountSuspended(DisplayPill, { props: { tag } });
47
+ expect(wrapper.element.tagName).toBe(tag.toUpperCase());
48
+ });
49
+
50
+ // ─── Base class ───────────────────────────────────────────────────────────
51
+
52
+ it("always has the display-pill class", async () => {
53
+ const wrapper = await mountSuspended(DisplayPill);
54
+ expect(wrapper.classes()).toContain("display-pill");
55
+ });
56
+
57
+ // ─── Label ────────────────────────────────────────────────────────────────
58
+
59
+ it("renders label text inside .pill-label", async () => {
60
+ const wrapper = await mountSuspended(DisplayPill, {
61
+ props: { label: "Active" },
62
+ });
63
+ expect(wrapper.find(".pill-label").text()).toBe("Active");
64
+ });
65
+
66
+ it("renders default slot when no label prop is set", async () => {
67
+ const wrapper = await mountSuspended(DisplayPill, {
68
+ slots: { default: "<strong class='custom'>Custom</strong>" },
69
+ });
70
+ expect(wrapper.find(".custom").exists()).toBe(true);
71
+ expect(wrapper.find(".pill-label").exists()).toBe(false);
72
+ });
73
+
74
+ it("does not render default slot when label prop is set", async () => {
75
+ const wrapper = await mountSuspended(DisplayPill, {
76
+ props: { label: "Active" },
77
+ slots: { default: "<strong class='custom'>Custom</strong>" },
78
+ });
79
+ expect(wrapper.find(".pill-label").exists()).toBe(true);
80
+ expect(wrapper.find(".custom").exists()).toBe(false);
81
+ });
82
+
83
+ // ─── Icon slot ────────────────────────────────────────────────────────────
84
+
85
+ it("renders icon slot content", async () => {
86
+ const wrapper = await mountSuspended(DisplayPill, {
87
+ slots: { icon: "<span class='test-icon'>★</span>" },
88
+ });
89
+ expect(wrapper.find(".test-icon").exists()).toBe(true);
90
+ });
91
+
92
+ // ─── Size ─────────────────────────────────────────────────────────────────
93
+
94
+ it("applies md size class by default", async () => {
95
+ const wrapper = await mountSuspended(DisplayPill);
96
+ expect(wrapper.classes()).toContain("md");
97
+ });
98
+
99
+ it.each(["sm", "md", "lg"] as const)("applies %s size class when size='%s'", async (size) => {
100
+ const wrapper = await mountSuspended(DisplayPill, { props: { size } });
101
+ expect(wrapper.classes()).toContain(size);
102
+ });
103
+
104
+ // ─── Variant ──────────────────────────────────────────────────────────────
105
+
106
+ it("applies default variant class by default", async () => {
107
+ const wrapper = await mountSuspended(DisplayPill);
108
+ expect(wrapper.classes()).toContain("default");
109
+ });
110
+
111
+ it.each(["default", "primary", "success", "warning", "danger", "neutral"] as const)(
112
+ "applies %s variant class when variant='%s'",
113
+ async (variant) => {
114
+ const wrapper = await mountSuspended(DisplayPill, { props: { variant } });
115
+ expect(wrapper.classes()).toContain(variant);
116
+ }
117
+ );
118
+
119
+ // ─── Reversed ─────────────────────────────────────────────────────────────
120
+
121
+ it("does not apply is-reversed class by default", async () => {
122
+ const wrapper = await mountSuspended(DisplayPill);
123
+ expect(wrapper.classes()).not.toContain("is-reversed");
124
+ });
125
+
126
+ it("applies is-reversed class when reversed=true", async () => {
127
+ const wrapper = await mountSuspended(DisplayPill, {
128
+ props: { reversed: true },
129
+ });
130
+ expect(wrapper.classes()).toContain("is-reversed");
131
+ });
132
+
133
+ // ─── styleClassPassthrough ────────────────────────────────────────────────
134
+
135
+ it("applies a single styleClassPassthrough string", async () => {
136
+ const wrapper = await mountSuspended(DisplayPill, {
137
+ props: { styleClassPassthrough: "featured" },
138
+ });
139
+ expect(wrapper.classes()).toContain("featured");
140
+ });
141
+
142
+ it("applies multiple styleClassPassthrough classes from an array", async () => {
143
+ const wrapper = await mountSuspended(DisplayPill, {
144
+ props: { styleClassPassthrough: ["featured", "highlighted"] },
145
+ });
146
+ expect(wrapper.classes()).toContain("featured");
147
+ expect(wrapper.classes()).toContain("highlighted");
148
+ });
149
+
150
+ it("updates classes when styleClassPassthrough prop changes", async () => {
151
+ const wrapper = await mountSuspended(DisplayPill, {
152
+ props: { styleClassPassthrough: ["featured"] },
153
+ });
154
+ expect(wrapper.classes()).toContain("featured");
155
+ await wrapper.setProps({ styleClassPassthrough: ["highlighted"] });
156
+ expect(wrapper.classes()).not.toContain("featured");
157
+ expect(wrapper.classes()).toContain("highlighted");
158
+ });
159
+ });
@@ -0,0 +1,7 @@
1
+ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
+
3
+ exports[`DisplayPill > renders correct HTML structure (default) 1`] = `"<span class="display-pill md default"><span class="pill-label">Status</span></span>"`;
4
+
5
+ exports[`DisplayPill > renders correct HTML structure (reversed) 1`] = `"<span class="display-pill md default is-reversed"><span class="icon">✓</span><span class="pill-label">Status</span></span>"`;
6
+
7
+ exports[`DisplayPill > renders correct HTML structure (with icon slot) 1`] = `"<span class="display-pill lg success"><span class="icon">✓</span><span class="pill-label">Status</span></span>"`;