srcdev-nuxt-components 9.5.7 → 9.6.1
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/component-compliance-checklist.md +8 -2
- package/.claude/skills/component-dynamic-slots.md +3 -3
- package/.claude/skills/component-export-types.md +10 -16
- package/.claude/skills/component-naming.md +1 -1
- package/.claude/skills/components/alert-content.md +9 -1
- package/.claude/skills/components/breadcrumb.md +5 -0
- package/.claude/skills/components/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +13 -7
- package/.claude/skills/components/display-chip.md +37 -24
- package/.claude/skills/components/display-pill.md +58 -8
- package/.claude/skills/components/display-prompt.md +8 -5
- package/.claude/skills/components/display-toast.md +21 -4
- package/.claude/skills/components/glowing-border.md +20 -2
- package/.claude/skills/components/input-button.md +3 -0
- package/.claude/skills/components/responsive-header.md +59 -0
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/service-detail.md +32 -0
- package/.claude/skills/components/service-summary.md +8 -0
- package/.claude/skills/components/services-card-grid.md +5 -3
- package/.claude/skills/components/services-card.md +22 -12
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/slider-gallery.md +10 -4
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +4 -5
- package/.claude/skills/storybook-add-story.md +53 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
- package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
- package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
- package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
- package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
- package/.vscode/srcdev-component-services-card.code-snippets +38 -36
- package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
- package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
- package/app/assets/styles/setup/03.theming/_status.css +24 -0
- package/app/assets/styles/setup/03.theming/index.css +1 -0
- package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
- package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
- package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
- package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
- package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
- package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
- package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
- package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
- package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
- package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
- package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
- package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
- package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
- package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
- package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
- package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
- package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
- package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
- package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
- package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
- package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
- package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
- package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
- package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
- package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
- package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
- package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
- package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
- package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
- package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
- package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
- package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
- package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
- package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
- package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
- package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
- package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
- package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
- package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
- package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
- package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
- package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
- package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
- package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
- package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
- package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
- package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
- package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
- package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
- package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
- package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
- package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
- package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
- package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
- package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
- package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
- package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
- package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
- package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
- package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
- package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
- package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
- package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
- package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
- package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
- package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
- package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
- package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
- package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
- package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
- package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
- package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
- package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
- package/app/components/05.forms/input-button/InputButton.vue +3 -1
- package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
- package/app/composables/useAnchorScroll.ts +1 -1
- package/app/composables/useTabs.ts +148 -206
- package/app/types/components/display-chip.d.ts +8 -4
- package/app/types/components/display-toast.d.ts +2 -0
- package/app/types/components/index.ts +0 -1
- package/app/types/components/select-menu.d.ts +2 -0
- package/app/utils/pausableTimeout.ts +41 -0
- package/nuxt.config.ts +1 -1
- package/package.json +1 -1
- package/.claude/skills/components/site-navigation.md +0 -145
- package/.claude/skills/components/tab-navigation.md +0 -272
- package/.claude/skills/components/tabs-core.md +0 -187
- package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
- package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
- package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
- package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
- package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
- package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
- package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
- package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
- package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
- package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
- package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
- package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
- package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
- package/app/composables/useNavCollapse.ts +0 -86
- package/app/types/components/nav-item.d.ts +0 -11
|
@@ -2,15 +2,20 @@
|
|
|
2
2
|
<component
|
|
3
3
|
:is="tag"
|
|
4
4
|
class="display-pill"
|
|
5
|
-
:class="
|
|
5
|
+
:class="elementClasses"
|
|
6
6
|
:type="tag === 'button' ? 'button' : undefined"
|
|
7
|
+
:data-size="size"
|
|
8
|
+
:data-variant="variant"
|
|
9
|
+
:data-reversed="reversed || undefined"
|
|
7
10
|
:data-icon-position="iconPosition()"
|
|
8
11
|
>
|
|
9
12
|
<span v-if="slots.icon" class="display-pill-icon">
|
|
10
13
|
<slot name="icon"></slot>
|
|
11
14
|
</span>
|
|
12
|
-
<span v-if="
|
|
13
|
-
|
|
15
|
+
<span v-if="hasText()" class="display-pill-label">
|
|
16
|
+
<template v-if="hasLabel">{{ label }}</template>
|
|
17
|
+
<slot v-else name="default"></slot>
|
|
18
|
+
</span>
|
|
14
19
|
</component>
|
|
15
20
|
</template>
|
|
16
21
|
|
|
@@ -35,10 +40,14 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
35
40
|
|
|
36
41
|
const slots = useSlots();
|
|
37
42
|
|
|
38
|
-
|
|
43
|
+
const hasLabel = computed(() => !!props.label?.trim());
|
|
44
|
+
|
|
45
|
+
// Functions, not computeds: slots aren't reactive, so a cached value goes stale when a slot is toggled.
|
|
46
|
+
const hasText = () => hasLabel.value || !!slots.default;
|
|
47
|
+
|
|
39
48
|
const iconPosition = () => {
|
|
40
49
|
if (!slots.icon) return undefined;
|
|
41
|
-
if (!
|
|
50
|
+
if (!hasText()) return "only";
|
|
42
51
|
return props.reversed ? "end" : "start";
|
|
43
52
|
};
|
|
44
53
|
|
|
@@ -55,6 +64,8 @@ watch(
|
|
|
55
64
|
.display-pill {
|
|
56
65
|
--_background: var(--display-pill-background, var(--slate-01));
|
|
57
66
|
--_text-colour: var(--display-pill-text-colour, var(--slate-09));
|
|
67
|
+
--_border-colour: var(--display-pill-border-colour, transparent);
|
|
68
|
+
--_ring-colour: var(--display-pill-ring-colour, var(--_background));
|
|
58
69
|
--_font-size: var(--display-pill-font-size, 1.2rem);
|
|
59
70
|
--_icon-size: var(--display-pill-icon-size, 1.4rem);
|
|
60
71
|
|
|
@@ -92,19 +103,21 @@ watch(
|
|
|
92
103
|
}
|
|
93
104
|
|
|
94
105
|
border: var(--display-pill-border-width, 0.1rem) var(--display-pill-border-style, solid)
|
|
95
|
-
var(--
|
|
106
|
+
var(--_border-colour);
|
|
96
107
|
/* 100vw always resolves to a full pill radius regardless of element size */
|
|
97
108
|
border-radius: var(--display-pill-border-radius, 100vw);
|
|
98
109
|
outline: var(--display-pill-outline, none);
|
|
99
110
|
outline-offset: var(--display-pill-outline-offset, 0);
|
|
111
|
+
/* box-shadow, not outline: outlines ignore border-radius before Safari 16.4. */
|
|
112
|
+
box-shadow: 0 0 0 var(--display-pill-ring-width, 0) var(--_ring-colour);
|
|
100
113
|
background-color: var(--_background);
|
|
101
114
|
color: var(--_text-colour);
|
|
102
115
|
font-family: inherit;
|
|
103
116
|
font-size: var(--_font-size);
|
|
104
117
|
font-weight: var(--display-pill-font-weight, 500);
|
|
105
118
|
line-height: 1;
|
|
106
|
-
white-space: nowrap;
|
|
107
119
|
width: fit-content;
|
|
120
|
+
max-inline-size: 100%;
|
|
108
121
|
cursor: default;
|
|
109
122
|
user-select: none;
|
|
110
123
|
|
|
@@ -117,55 +130,78 @@ watch(
|
|
|
117
130
|
}
|
|
118
131
|
}
|
|
119
132
|
|
|
120
|
-
|
|
133
|
+
&:empty {
|
|
134
|
+
display: none;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
&[data-reversed] {
|
|
121
138
|
flex-direction: row-reverse;
|
|
122
139
|
}
|
|
123
140
|
|
|
124
141
|
.display-pill-icon {
|
|
125
142
|
display: inline-flex;
|
|
143
|
+
flex-shrink: 0;
|
|
126
144
|
align-items: center;
|
|
127
145
|
font-size: var(--_icon-size);
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
.display-pill-label {
|
|
131
|
-
display:
|
|
132
|
-
|
|
149
|
+
display: -webkit-box;
|
|
150
|
+
-webkit-box-orient: vertical;
|
|
151
|
+
-webkit-line-clamp: var(--display-pill-label-line-clamp, 1);
|
|
152
|
+
line-clamp: var(--display-pill-label-line-clamp, 1);
|
|
153
|
+
overflow: hidden;
|
|
154
|
+
overflow-wrap: anywhere;
|
|
155
|
+
min-inline-size: 0;
|
|
156
|
+
/* Room for descenders and accents, which line-height: 1 would otherwise clip. */
|
|
157
|
+
padding-block: 0.15em;
|
|
158
|
+
margin-block: -0.15em;
|
|
133
159
|
}
|
|
134
160
|
|
|
135
|
-
|
|
161
|
+
&[data-size="sm"] {
|
|
136
162
|
--_font-size: var(--display-pill-font-size-sm, 1rem);
|
|
137
163
|
--_icon-size: var(--display-pill-icon-size-sm, 1.2rem);
|
|
138
164
|
}
|
|
139
165
|
|
|
140
|
-
|
|
166
|
+
&[data-size="lg"] {
|
|
141
167
|
--_font-size: var(--display-pill-font-size-lg, 1.4rem);
|
|
142
168
|
--_icon-size: var(--display-pill-icon-size-lg, 1.6rem);
|
|
143
169
|
}
|
|
144
170
|
|
|
145
171
|
/* Variant token, then the base token, then the variant's own default. */
|
|
146
|
-
|
|
172
|
+
&[data-variant="primary"] {
|
|
147
173
|
--_background: var(--display-pill-primary-background, var(--display-pill-background, var(--blue-01)));
|
|
148
174
|
--_text-colour: var(--display-pill-primary-text-colour, var(--display-pill-text-colour, var(--blue-09)));
|
|
175
|
+
--_border-colour: var(--display-pill-primary-border-colour, var(--display-pill-border-colour, transparent));
|
|
176
|
+
--_ring-colour: var(--display-pill-primary-ring-colour, var(--display-pill-ring-colour, var(--_background)));
|
|
149
177
|
}
|
|
150
178
|
|
|
151
|
-
|
|
152
|
-
--_background: var(--display-pill-success-background, var(--display-pill-background, var(--
|
|
153
|
-
--_text-colour: var(--display-pill-success-text-colour, var(--display-pill-text-colour, var(--
|
|
179
|
+
&[data-variant="success"] {
|
|
180
|
+
--_background: var(--display-pill-success-background, var(--display-pill-background, var(--status-success-surface)));
|
|
181
|
+
--_text-colour: var(--display-pill-success-text-colour, var(--display-pill-text-colour, var(--status-success-text)));
|
|
182
|
+
--_border-colour: var(--display-pill-success-border-colour, var(--display-pill-border-colour, transparent));
|
|
183
|
+
--_ring-colour: var(--display-pill-success-ring-colour, var(--display-pill-ring-colour, var(--_background)));
|
|
154
184
|
}
|
|
155
185
|
|
|
156
|
-
|
|
157
|
-
--_background: var(--display-pill-warning-background, var(--display-pill-background, var(--
|
|
158
|
-
--_text-colour: var(--display-pill-warning-text-colour, var(--display-pill-text-colour, var(--
|
|
186
|
+
&[data-variant="warning"] {
|
|
187
|
+
--_background: var(--display-pill-warning-background, var(--display-pill-background, var(--status-warning-surface)));
|
|
188
|
+
--_text-colour: var(--display-pill-warning-text-colour, var(--display-pill-text-colour, var(--status-warning-text)));
|
|
189
|
+
--_border-colour: var(--display-pill-warning-border-colour, var(--display-pill-border-colour, transparent));
|
|
190
|
+
--_ring-colour: var(--display-pill-warning-ring-colour, var(--display-pill-ring-colour, var(--_background)));
|
|
159
191
|
}
|
|
160
192
|
|
|
161
|
-
|
|
162
|
-
--_background: var(--display-pill-danger-background, var(--display-pill-background, var(--
|
|
163
|
-
--_text-colour: var(--display-pill-danger-text-colour, var(--display-pill-text-colour, var(--
|
|
193
|
+
&[data-variant="danger"] {
|
|
194
|
+
--_background: var(--display-pill-danger-background, var(--display-pill-background, var(--status-danger-surface)));
|
|
195
|
+
--_text-colour: var(--display-pill-danger-text-colour, var(--display-pill-text-colour, var(--status-danger-text)));
|
|
196
|
+
--_border-colour: var(--display-pill-danger-border-colour, var(--display-pill-border-colour, transparent));
|
|
197
|
+
--_ring-colour: var(--display-pill-danger-ring-colour, var(--display-pill-ring-colour, var(--_background)));
|
|
164
198
|
}
|
|
165
199
|
|
|
166
|
-
|
|
200
|
+
&[data-variant="neutral"] {
|
|
167
201
|
--_background: var(--display-pill-neutral-background, var(--display-pill-background, var(--slate-08)));
|
|
168
202
|
--_text-colour: var(--display-pill-neutral-text-colour, var(--display-pill-text-colour, var(--slate-03)));
|
|
203
|
+
--_border-colour: var(--display-pill-neutral-border-colour, var(--display-pill-border-colour, transparent));
|
|
204
|
+
--_ring-colour: var(--display-pill-neutral-ring-colour, var(--display-pill-ring-colour, var(--_background)));
|
|
169
205
|
}
|
|
170
206
|
}
|
|
171
207
|
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Meta, StoryFn } from "@nuxtjs/storybook";
|
|
2
|
+
import { computed } from "vue";
|
|
2
3
|
import StorybookComponent from "../DisplayPill.vue";
|
|
3
4
|
|
|
5
|
+
type LineClamp = "none" | "1" | "2" | "3";
|
|
6
|
+
|
|
7
|
+
// labelLineClamp is story-only: it drives a CSS token, not a DisplayPill prop. See its argTypes entry.
|
|
4
8
|
type StoryArgs = {
|
|
5
9
|
tag: "span" | "div" | "button" | "a";
|
|
6
10
|
label: string;
|
|
@@ -8,12 +12,32 @@ type StoryArgs = {
|
|
|
8
12
|
variant: "default" | "primary" | "success" | "warning" | "danger" | "neutral";
|
|
9
13
|
reversed: boolean;
|
|
10
14
|
showIcon: boolean;
|
|
15
|
+
labelLineClamp: LineClamp;
|
|
16
|
+
ring?: boolean;
|
|
11
17
|
};
|
|
12
18
|
|
|
13
19
|
export default {
|
|
14
20
|
title: "Atoms/DisplayPill",
|
|
15
21
|
component: StorybookComponent,
|
|
22
|
+
parameters: {
|
|
23
|
+
docs: {
|
|
24
|
+
description: {
|
|
25
|
+
component:
|
|
26
|
+
"Used for the neutral duration and price pills in [ServiceSummary](?path=/story/organisms-services-service-summary--default) " +
|
|
27
|
+
"and [ServiceDetail](?path=/story/organisms-services-service-detail--default), which restyle them through their own pill tokens.",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
16
31
|
argTypes: {
|
|
32
|
+
labelLineClamp: {
|
|
33
|
+
control: "select",
|
|
34
|
+
options: ["none", "1", "2", "3"] satisfies LineClamp[],
|
|
35
|
+
description:
|
|
36
|
+
"**Story control, not a prop.** Sets the `--display-pill-label-line-clamp` CSS token on a wrapper so you can try it here. " +
|
|
37
|
+
"To use it in an app, set the token in your own CSS, e.g. `.tag-list { --display-pill-label-line-clamp: none; }`. " +
|
|
38
|
+
"`1` (the default) is single-line ellipsis, `none` lets a long label wrap.",
|
|
39
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "1" } },
|
|
40
|
+
},
|
|
17
41
|
tag: {
|
|
18
42
|
control: { type: "inline-radio" },
|
|
19
43
|
options: ["span", "div", "button", "a"],
|
|
@@ -58,16 +82,30 @@ export default {
|
|
|
58
82
|
variant: "default",
|
|
59
83
|
reversed: false,
|
|
60
84
|
showIcon: true,
|
|
85
|
+
labelLineClamp: "1",
|
|
61
86
|
},
|
|
62
87
|
} as Meta<StoryArgs>;
|
|
63
88
|
|
|
89
|
+
// Links open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
90
|
+
const usedByNote = `
|
|
91
|
+
<p style="margin: 0; flex-basis: 100%; text-align: center; font-size: 1.4rem;">
|
|
92
|
+
In use: the duration and price pills in the
|
|
93
|
+
<a href="/?path=/story/organisms-services-service-summary--default" target="_top">ServiceSummary</a> and
|
|
94
|
+
<a href="/?path=/story/organisms-services-service-detail--default" target="_top">ServiceDetail</a> stories.
|
|
95
|
+
</p>
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
const tokenStyles = (args: StoryArgs) =>
|
|
99
|
+
computed(() => ({ "--display-pill-label-line-clamp": args.labelLineClamp }));
|
|
100
|
+
|
|
64
101
|
const Template: StoryFn<StoryArgs> = (args) => ({
|
|
65
102
|
components: { StorybookComponent },
|
|
66
103
|
setup() {
|
|
67
|
-
return { args };
|
|
104
|
+
return { args, tokenStyles: tokenStyles(args) };
|
|
68
105
|
},
|
|
69
106
|
template: `
|
|
70
|
-
<div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
|
|
107
|
+
<div :style="tokenStyles" style="display: flex; align-items: center; align-content: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
|
|
108
|
+
${usedByNote}
|
|
71
109
|
<StorybookComponent
|
|
72
110
|
:tag="args.tag"
|
|
73
111
|
:label="args.label"
|
|
@@ -90,10 +128,11 @@ const variants = ["default", "primary", "success", "warning", "danger", "neutral
|
|
|
90
128
|
export const AllVariants: StoryFn<StoryArgs> = (args) => ({
|
|
91
129
|
components: { StorybookComponent },
|
|
92
130
|
setup() {
|
|
93
|
-
return { args, variants };
|
|
131
|
+
return { args, variants, tokenStyles: tokenStyles(args) };
|
|
94
132
|
},
|
|
95
133
|
template: `
|
|
96
|
-
<div style="display: flex; align-items: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
|
|
134
|
+
<div :style="tokenStyles" style="display: flex; align-items: center; align-content: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap;">
|
|
135
|
+
${usedByNote}
|
|
97
136
|
<StorybookComponent
|
|
98
137
|
v-for="variant in variants"
|
|
99
138
|
:key="variant"
|
|
@@ -112,30 +151,215 @@ export const AllVariants: StoryFn<StoryArgs> = (args) => ({
|
|
|
112
151
|
});
|
|
113
152
|
AllVariants.argTypes = { variant: { table: { disable: true } }, label: { table: { disable: true } } };
|
|
114
153
|
|
|
115
|
-
|
|
154
|
+
// Each variant keeps its own text colour, so currentColor borders follow the variant. Neutral's text is a light
|
|
155
|
+
// step (it's the dark, inverted pill), so it's darkened here to stay visible once its background is transparent.
|
|
156
|
+
const tokenShowcase =
|
|
157
|
+
(tokens: string, argTokens: (args: StoryArgs) => Record<string, string> = () => ({})): StoryFn<StoryArgs> =>
|
|
158
|
+
(args) => ({
|
|
116
159
|
components: { StorybookComponent },
|
|
117
160
|
setup() {
|
|
118
|
-
return { args, variants };
|
|
161
|
+
return { args, variants, argStyles: computed(() => argTokens(args)) };
|
|
119
162
|
},
|
|
120
163
|
template: `
|
|
121
|
-
<div
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
164
|
+
<div :style="argStyles" style="display: flex; align-items: center; justify-content: center; min-height: 100vh; gap: 1.6rem; flex-wrap: wrap; ${tokens}">
|
|
165
|
+
<StorybookComponent
|
|
166
|
+
v-for="variant in variants"
|
|
167
|
+
:key="variant"
|
|
168
|
+
:tag="args.tag"
|
|
169
|
+
:label="variant"
|
|
170
|
+
:size="args.size"
|
|
171
|
+
:variant="variant"
|
|
172
|
+
:reversed="args.reversed"
|
|
173
|
+
>
|
|
174
|
+
<template v-if="args.showIcon" #icon>
|
|
175
|
+
<Icon name="material-symbols:check-circle-outline" />
|
|
176
|
+
</template>
|
|
177
|
+
</StorybookComponent>
|
|
130
178
|
</div>
|
|
131
179
|
`,
|
|
132
180
|
});
|
|
181
|
+
|
|
182
|
+
const showcaseArgTypes = {
|
|
183
|
+
variant: { table: { disable: true } },
|
|
184
|
+
label: { table: { disable: true } },
|
|
185
|
+
labelLineClamp: { table: { disable: true } },
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export const OutlinedViaBaseTokens = tokenShowcase(`
|
|
189
|
+
--display-pill-background: transparent;
|
|
190
|
+
--display-pill-border-width: 0.2rem;
|
|
191
|
+
--display-pill-border-colour: var(--slate-05);
|
|
192
|
+
--display-pill-primary-border-colour: var(--blue-06);
|
|
193
|
+
--display-pill-success-border-colour: var(--status-success);
|
|
194
|
+
--display-pill-warning-border-colour: var(--status-warning);
|
|
195
|
+
--display-pill-danger-border-colour: var(--status-danger);
|
|
196
|
+
--display-pill-neutral-border-colour: var(--slate-08);
|
|
197
|
+
--display-pill-neutral-text-colour: var(--slate-08);
|
|
198
|
+
`);
|
|
133
199
|
OutlinedViaBaseTokens.storyName = "Outlined (base tokens override every variant)";
|
|
200
|
+
OutlinedViaBaseTokens.argTypes = showcaseArgTypes;
|
|
134
201
|
OutlinedViaBaseTokens.parameters = {
|
|
135
202
|
docs: {
|
|
136
203
|
description: {
|
|
137
204
|
story:
|
|
138
|
-
"
|
|
205
|
+
"A transparent base --display-pill-background on an ancestor clears every variant's fill; --display-pill-border-colour sets a base border, " +
|
|
206
|
+
"and the per-variant --display-pill-<variant>-border-colour tokens give each variant its own mid-step status colour while the text keeps its darker step. " +
|
|
207
|
+
"--display-pill-neutral-text-colour darkens the neutral pill, whose default text is light because it's normally a dark, inverted pill.",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
// A 2px ring in each variant's light -02 step, toggled by the story-only ring control.
|
|
213
|
+
const ring02Tokens: Record<string, string> = {
|
|
214
|
+
"--display-pill-ring-width": "0.2rem",
|
|
215
|
+
"--display-pill-ring-colour": "var(--slate-02)",
|
|
216
|
+
"--display-pill-primary-ring-colour": "var(--blue-02)",
|
|
217
|
+
"--display-pill-success-ring-colour": "var(--green-02)",
|
|
218
|
+
"--display-pill-warning-ring-colour": "var(--orange-02)",
|
|
219
|
+
"--display-pill-danger-ring-colour": "var(--red-02)",
|
|
220
|
+
"--display-pill-neutral-ring-colour": "var(--slate-02)",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
const ringArgTypes = {
|
|
224
|
+
...showcaseArgTypes,
|
|
225
|
+
ring: {
|
|
226
|
+
control: "boolean" as const,
|
|
227
|
+
description:
|
|
228
|
+
"**Story control, not a prop.** Sets `--display-pill-ring-width: 0.2rem` and `--display-pill-<variant>-ring-colour` to each variant's `-02` step " +
|
|
229
|
+
"(`var(--blue-02)`, `var(--green-02)`...) on a wrapper. In an app, set the same tokens in your CSS, e.g. `.tag-list { --display-pill-ring-width: 0.2rem; }`.",
|
|
230
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "off" } },
|
|
231
|
+
},
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const ringArgTokens = (args: StoryArgs): Record<string, string> => (args.ring ? ring02Tokens : {});
|
|
235
|
+
|
|
236
|
+
export const OutlinedMonochrome = tokenShowcase(`
|
|
237
|
+
--display-pill-background: transparent;
|
|
238
|
+
--display-pill-border-colour: currentColor;
|
|
239
|
+
--display-pill-text-colour: var(--slate-07);
|
|
240
|
+
--display-pill-primary-text-colour: var(--blue-07);
|
|
241
|
+
--display-pill-success-text-colour: var(--green-07);
|
|
242
|
+
--display-pill-warning-text-colour: var(--orange-07);
|
|
243
|
+
--display-pill-danger-text-colour: var(--red-07);
|
|
244
|
+
--display-pill-neutral-text-colour: var(--slate-10);
|
|
245
|
+
`, ringArgTokens);
|
|
246
|
+
OutlinedMonochrome.storyName = "Outlined monochrome (mid-step colours)";
|
|
247
|
+
OutlinedMonochrome.args = { ring: false };
|
|
248
|
+
OutlinedMonochrome.argTypes = ringArgTypes;
|
|
249
|
+
OutlinedMonochrome.parameters = {
|
|
250
|
+
docs: {
|
|
251
|
+
description: {
|
|
252
|
+
story:
|
|
253
|
+
"Border and text share one colour per variant (--display-pill-border-colour: currentColor on a transparent background). " +
|
|
254
|
+
"The default text steps (-09/-10) are near-black in every hue, so outlined like this the variants look alike. " +
|
|
255
|
+
"Each text token is lifted to its -07 step (all at 48% lightness, so contrast stays even across variants); the border follows because it's currentColor. " +
|
|
256
|
+
"default and neutral are both slate, so neutral uses the darker -10 step to stay distinct.",
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
export const RingedTwoTone = tokenShowcase(`
|
|
262
|
+
--display-pill-border-width: 0.1rem;
|
|
263
|
+
--display-pill-border-colour: currentColor;
|
|
264
|
+
--display-pill-ring-width: 0.2rem;
|
|
265
|
+
`, ringArgTokens);
|
|
266
|
+
RingedTwoTone.storyName = "Ringed (dark border, light ring, per variant)";
|
|
267
|
+
RingedTwoTone.args = { ring: false };
|
|
268
|
+
RingedTwoTone.argTypes = ringArgTypes;
|
|
269
|
+
RingedTwoTone.parameters = {
|
|
270
|
+
docs: {
|
|
271
|
+
description: {
|
|
272
|
+
story:
|
|
273
|
+
"A 1px border in the variant's dark text colour (--display-pill-border-colour: currentColor) inside a 2px ring in the variant's light background colour " +
|
|
274
|
+
"(--display-pill-ring-width: 0.2rem; --display-pill-ring-colour defaults to the pill's background). The neutral pill is inverted, so its border is light and its ring dark. " +
|
|
275
|
+
"The ring control (a CSS token, not a prop) swaps the ring to each variant's -02 step, a shade darker than the fill, so it reads as a separate ring. " +
|
|
276
|
+
"The ring is a box-shadow, so it takes no layout space: leave room around ringed pills with gap or margin.",
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const longGerman = "Haarverlängerungsberatungstermin";
|
|
282
|
+
|
|
283
|
+
type StressCase = {
|
|
284
|
+
key: string;
|
|
285
|
+
caption: string;
|
|
286
|
+
label?: string;
|
|
287
|
+
slotHtml?: string;
|
|
288
|
+
icon?: string;
|
|
289
|
+
variant?: string;
|
|
290
|
+
size?: "sm" | "md" | "lg";
|
|
291
|
+
tag?: "span" | "button" | "a";
|
|
292
|
+
reversed?: boolean;
|
|
293
|
+
dir?: "rtl";
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const stressCases: StressCase[] = [
|
|
297
|
+
{ key: "german", caption: "Long German words", label: `${longGerman} und ${longGerman}`, icon: "material-symbols:schedule-outline" },
|
|
298
|
+
{ key: "url", caption: "Unbroken URL", label: "https://example.com/a/very/long/path/that/never/breaks/anywhere/at/all" },
|
|
299
|
+
{ key: "price", caption: "Long price (reversed)", label: "Ab 1.250,00 € pro Sitzung", icon: "material-symbols:sell-outline", reversed: true },
|
|
300
|
+
{ key: "emoji", caption: "Emoji first", label: "👩👩👧👦🇬🇧🇫🇷 Family plan", variant: "primary" },
|
|
301
|
+
{ key: "html", caption: "HTML-like text", label: "<script>alert('xss')</script>", variant: "danger" },
|
|
302
|
+
{ key: "rtl", caption: "Right-to-left", label: "تسريحات شعر جديدة لموسم الخريف", icon: "material-symbols:check-circle-outline", dir: "rtl", variant: "success" },
|
|
303
|
+
{ key: "accents", caption: "Descenders and accents", label: "Ägypten · gypsy · ÇĄĘ · Ýjqpg", variant: "warning", size: "lg" },
|
|
304
|
+
{ key: "single", caption: "Single character", label: "A", variant: "neutral" },
|
|
305
|
+
{ key: "empty", caption: "Empty label (renders nothing)", label: "" },
|
|
306
|
+
{ key: "blank", caption: "Whitespace label + icon (icon-only)", label: " ", icon: "material-symbols:star-outline", variant: "primary" },
|
|
307
|
+
{ key: "brokenIcon", caption: "Broken icon name", label: "Missing icon", icon: "material-symbols:this-icon-does-not-exist" },
|
|
308
|
+
{ key: "slot", caption: "Long default slot", slotHtml: `<strong>Pro</strong> plan with ${longGerman}`, icon: "material-symbols:star", variant: "primary" },
|
|
309
|
+
{ key: "invalidVariant", caption: "Unknown variant from a CMS", label: "variant: info", variant: "info" },
|
|
310
|
+
{ key: "button", caption: "Button, small", label: `Filter: ${longGerman}`, tag: "button", size: "sm" },
|
|
311
|
+
{ key: "link", caption: "Link, large", label: `${longGerman}${longGerman}`, tag: "a", size: "lg", variant: "neutral" },
|
|
312
|
+
];
|
|
313
|
+
|
|
314
|
+
const stressWidths = ["32rem", "16rem", "8rem"];
|
|
315
|
+
|
|
316
|
+
export const StressTest: StoryFn<StoryArgs> = (args) => ({
|
|
317
|
+
components: { StorybookComponent },
|
|
318
|
+
setup() {
|
|
319
|
+
return { stressCases, stressWidths, tokenStyles: tokenStyles(args) };
|
|
320
|
+
},
|
|
321
|
+
template: `
|
|
322
|
+
<div :style="tokenStyles" style="display: grid; gap: 2.4rem; padding: 3.2rem 2rem; font: 1.2rem sans-serif;">
|
|
323
|
+
<div style="display: grid; grid-template-columns: 16rem repeat(3, max-content); gap: 1.2rem 2.4rem; align-items: start;">
|
|
324
|
+
<span></span>
|
|
325
|
+
<strong v-for="width in stressWidths" :key="width">{{ width }} wide</strong>
|
|
326
|
+
<template v-for="item in stressCases" :key="item.key">
|
|
327
|
+
<span style="overflow-wrap: anywhere;">{{ item.caption }}</span>
|
|
328
|
+
<div
|
|
329
|
+
v-for="width in stressWidths"
|
|
330
|
+
:key="width"
|
|
331
|
+
:dir="item.dir"
|
|
332
|
+
:style="{ inlineSize: width, outline: '1px dashed currentColor', padding: '0.4rem' }"
|
|
333
|
+
>
|
|
334
|
+
<StorybookComponent
|
|
335
|
+
:label="item.label"
|
|
336
|
+
:variant="item.variant"
|
|
337
|
+
:size="item.size ?? 'md'"
|
|
338
|
+
:tag="item.tag ?? 'span'"
|
|
339
|
+
:href="item.tag === 'a' ? '#' : undefined"
|
|
340
|
+
:reversed="item.reversed ?? false"
|
|
341
|
+
>
|
|
342
|
+
<template v-if="item.icon" #icon><Icon :name="item.icon" /></template>
|
|
343
|
+
<template v-if="item.slotHtml" #default><span v-html="item.slotHtml"></span></template>
|
|
344
|
+
</StorybookComponent>
|
|
345
|
+
</div>
|
|
346
|
+
</template>
|
|
347
|
+
</div>
|
|
348
|
+
</div>
|
|
349
|
+
`,
|
|
350
|
+
});
|
|
351
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
352
|
+
StressTest.parameters = {
|
|
353
|
+
controls: { include: ["labelLineClamp"] },
|
|
354
|
+
docs: {
|
|
355
|
+
description: {
|
|
356
|
+
story:
|
|
357
|
+
"Hostile labels at three container widths: long German words, an unbroken URL, a long reversed price, emoji, HTML-like and right-to-left text, " +
|
|
358
|
+
"descenders and accents, a single character, empty and whitespace-only labels, a broken icon name, a long default slot, an unknown variant, " +
|
|
359
|
+
"and small button and large link pills. Check that no pill grows past its dashed box, long text ends in an ellipsis on one line, " +
|
|
360
|
+
"descenders and accents aren't clipped, the empty label renders no pill, the whitespace label becomes an icon-only pill, " +
|
|
361
|
+
"the unknown variant falls back to the default colours, and the icon never shrinks. " +
|
|
362
|
+
"Set the labelLineClamp control (a CSS token, not a prop) to none to see long labels wrap instead.",
|
|
139
363
|
},
|
|
140
364
|
},
|
|
141
365
|
};
|
|
@@ -64,14 +64,32 @@ describe("DisplayPill", () => {
|
|
|
64
64
|
expect(wrapper.find(".display-pill-label").text()).toBe("Active");
|
|
65
65
|
});
|
|
66
66
|
|
|
67
|
-
it("renders default slot when no label prop is set", async () => {
|
|
67
|
+
it("renders default slot inside .display-pill-label when no label prop is set", async () => {
|
|
68
68
|
const wrapper = await mountSuspended(DisplayPill, {
|
|
69
69
|
slots: { default: "<strong class='custom'>Custom</strong>" },
|
|
70
70
|
});
|
|
71
|
-
expect(wrapper.find(".custom").exists()).toBe(true);
|
|
71
|
+
expect(wrapper.find(".display-pill-label .custom").exists()).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("treats a whitespace-only label as no label and falls back to the default slot", async () => {
|
|
75
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
76
|
+
props: { label: " " },
|
|
77
|
+
slots: { default: "<strong class='custom'>Custom</strong>" },
|
|
78
|
+
});
|
|
79
|
+
expect(wrapper.find(".display-pill-label .custom").exists()).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it.each(["", " "])("renders no label element for label=%j with no default slot", async (label) => {
|
|
83
|
+
const wrapper = await mountSuspended(DisplayPill, { props: { label } });
|
|
72
84
|
expect(wrapper.find(".display-pill-label").exists()).toBe(false);
|
|
73
85
|
});
|
|
74
86
|
|
|
87
|
+
it("renders a label with HTML-like text as text, not markup", async () => {
|
|
88
|
+
const wrapper = await mountSuspended(DisplayPill, { props: { label: "<b>bold</b>" } });
|
|
89
|
+
expect(wrapper.find(".display-pill-label").text()).toBe("<b>bold</b>");
|
|
90
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
75
93
|
it("does not render default slot when label prop is set", async () => {
|
|
76
94
|
const wrapper = await mountSuspended(DisplayPill, {
|
|
77
95
|
props: { label: "Active" },
|
|
@@ -92,43 +110,46 @@ describe("DisplayPill", () => {
|
|
|
92
110
|
|
|
93
111
|
// ─── Size ─────────────────────────────────────────────────────────────────
|
|
94
112
|
|
|
95
|
-
it("
|
|
113
|
+
it("sets data-size='md' by default", async () => {
|
|
96
114
|
const wrapper = await mountSuspended(DisplayPill);
|
|
97
|
-
expect(wrapper.
|
|
115
|
+
expect(wrapper.attributes("data-size")).toBe("md");
|
|
98
116
|
});
|
|
99
117
|
|
|
100
|
-
it.each(["sm", "md", "lg"] as const)("
|
|
118
|
+
it.each(["sm", "md", "lg"] as const)("sets data-size='%s' when size='%s'", async (size) => {
|
|
101
119
|
const wrapper = await mountSuspended(DisplayPill, { props: { size } });
|
|
102
|
-
expect(wrapper.
|
|
120
|
+
expect(wrapper.attributes("data-size")).toBe(size);
|
|
121
|
+
expect(wrapper.classes()).not.toContain(size);
|
|
103
122
|
});
|
|
104
123
|
|
|
105
124
|
// ─── Variant ──────────────────────────────────────────────────────────────
|
|
106
125
|
|
|
107
|
-
it("
|
|
126
|
+
it("sets data-variant='default' by default", async () => {
|
|
108
127
|
const wrapper = await mountSuspended(DisplayPill);
|
|
109
|
-
expect(wrapper.
|
|
128
|
+
expect(wrapper.attributes("data-variant")).toBe("default");
|
|
110
129
|
});
|
|
111
130
|
|
|
112
131
|
it.each(["default", "primary", "success", "warning", "danger", "neutral"] as const)(
|
|
113
|
-
"
|
|
132
|
+
"sets data-variant='%s' when variant='%s'",
|
|
114
133
|
async (variant) => {
|
|
115
134
|
const wrapper = await mountSuspended(DisplayPill, { props: { variant } });
|
|
116
|
-
expect(wrapper.
|
|
135
|
+
expect(wrapper.attributes("data-variant")).toBe(variant);
|
|
136
|
+
expect(wrapper.classes()).not.toContain(variant);
|
|
117
137
|
}
|
|
118
138
|
);
|
|
119
139
|
|
|
120
140
|
// ─── Reversed ─────────────────────────────────────────────────────────────
|
|
121
141
|
|
|
122
|
-
it("does not
|
|
142
|
+
it("does not set data-reversed by default", async () => {
|
|
123
143
|
const wrapper = await mountSuspended(DisplayPill);
|
|
124
|
-
expect(wrapper.
|
|
144
|
+
expect(wrapper.attributes("data-reversed")).toBeUndefined();
|
|
125
145
|
});
|
|
126
146
|
|
|
127
|
-
it("
|
|
147
|
+
it("sets data-reversed when reversed=true", async () => {
|
|
128
148
|
const wrapper = await mountSuspended(DisplayPill, {
|
|
129
149
|
props: { reversed: true },
|
|
130
150
|
});
|
|
131
|
-
expect(wrapper.
|
|
151
|
+
expect(wrapper.attributes("data-reversed")).toBeDefined();
|
|
152
|
+
expect(wrapper.classes()).not.toContain("is-reversed");
|
|
132
153
|
});
|
|
133
154
|
|
|
134
155
|
// ─── styleClassPassthrough ────────────────────────────────────────────────
|
|
@@ -219,6 +240,14 @@ describe("DisplayPill", () => {
|
|
|
219
240
|
expect(wrapper.attributes("data-icon-position")).toBe("only");
|
|
220
241
|
});
|
|
221
242
|
|
|
243
|
+
it("sets data-icon-position='only' when the label is whitespace", async () => {
|
|
244
|
+
const wrapper = await mountSuspended(DisplayPill, {
|
|
245
|
+
props: { label: " " },
|
|
246
|
+
slots: { icon: "<span>●</span>" },
|
|
247
|
+
});
|
|
248
|
+
expect(wrapper.attributes("data-icon-position")).toBe("only");
|
|
249
|
+
});
|
|
250
|
+
|
|
222
251
|
it("treats default slot text as a label for icon position", async () => {
|
|
223
252
|
const wrapper = await mountSuspended(DisplayPill, {
|
|
224
253
|
slots: { icon: "<span>●</span>", default: "Pro plan" },
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`DisplayPill > renders correct HTML structure (default) 1`] = `"<span class="display-pill md default"><!--v-if--><span class="display-pill-label">Status</span></span>"`;
|
|
3
|
+
exports[`DisplayPill > renders correct HTML structure (default) 1`] = `"<span class="display-pill" data-size="md" data-variant="default"><!--v-if--><span class="display-pill-label">Status</span></span>"`;
|
|
4
4
|
|
|
5
|
-
exports[`DisplayPill > renders correct HTML structure (reversed) 1`] = `"<span class="display-pill md default
|
|
5
|
+
exports[`DisplayPill > renders correct HTML structure (reversed) 1`] = `"<span class="display-pill" data-size="md" data-variant="default" data-reversed="true" data-icon-position="end"><span class="display-pill-icon"><span class="icon">✓</span></span><span class="display-pill-label">Status</span></span>"`;
|
|
6
6
|
|
|
7
|
-
exports[`DisplayPill > renders correct HTML structure (with icon slot) 1`] = `"<span class="display-pill lg success" data-icon-position="start"><span class="display-pill-icon"><span class="icon">✓</span></span><span class="display-pill-label">Status</span></span>"`;
|
|
7
|
+
exports[`DisplayPill > renders correct HTML structure (with icon slot) 1`] = `"<span class="display-pill" data-size="lg" data-variant="success" data-icon-position="start"><span class="display-pill-icon"><span class="icon">✓</span></span><span class="display-pill-label">Status</span></span>"`;
|