srcdev-nuxt-components 9.6.0 → 9.6.2
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 +22 -3
- 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/container-glow.md +21 -8
- package/.claude/skills/components/display-avatar.md +27 -10
- package/.claude/skills/components/display-chip.md +38 -24
- package/.claude/skills/components/display-pill.md +58 -10
- 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/google-reviews.md +2 -2
- package/.claude/skills/components/profile-section.md +59 -21
- package/.claude/skills/components/responsive-header.md +11 -1
- package/.claude/skills/components/select-menu.md +54 -5
- package/.claude/skills/components/site-header.md +2 -2
- package/.claude/skills/components/social-icons-list.md +2 -0
- package/.claude/skills/components/tabbed-content.md +300 -0
- package/.claude/skills/composable-anchor-scroll.md +16 -15
- package/.claude/skills/index.md +8 -7
- package/.claude/skills/storybook-add-story.md +55 -0
- package/.claude/skills/theming-component-token-pattern.md +48 -0
- package/.claude/skills/theming-status-tokens.md +60 -0
- package/.vscode/srcdev-component-display-avatar.code-snippets +9 -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-profile-section.code-snippets +27 -4
- package/.vscode/srcdev-component-responsive-header.code-snippets +6 -0
- package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
- 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 +75 -9
- package/app/components/01.atoms/display-pill/DisplayPill.vue +57 -24
- 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/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 +33 -15
- package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +81 -36
- package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +108 -6
- package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +104 -13
- package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +4 -3
- package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +57 -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/profile-section/CONSUMER-STYLING.md +103 -13
- package/app/components/02.molecules/profile-section/ProfileSection.vue +123 -23
- package/app/components/02.molecules/profile-section/stories/ProfileSection.stories.ts +353 -115
- package/app/components/02.molecules/profile-section/tests/ProfileSection.spec.ts +144 -11
- package/app/components/02.molecules/profile-section/tests/__snapshots__/ProfileSection.spec.ts.snap +6 -11
- 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/social-icons-list/CONSUMER-STYLING.md +6 -2
- package/app/components/02.molecules/social-icons-list/SocialIconsList.vue +1 -0
- 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/google-reviews/tests/__snapshots__/GoogleReviewCard.spec.ts.snap +1 -1
- package/app/components/03.organisms/google-reviews/tests/__snapshots__/GoogleReviews.spec.ts.snap +6 -6
- package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +38 -11
- package/app/components/03.organisms/responsive-header/NavigationItems.vue +27 -12
- package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +35 -20
- package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +1 -1
- package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +1 -1
- package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +3 -3
- 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/tab-navigation.md +0 -288
- package/.claude/skills/components/tabs-core.md +0 -187
- 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/tab-navigation/CONSUMER-STYLING.md +0 -191
- package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -550
- package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -259
- package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -379
- 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
|
@@ -1,24 +1,35 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<component
|
|
3
3
|
:is="chip ? DisplayChip : as"
|
|
4
|
-
v-bind="chip ? { tag: chipTag, config: chipConfig } : {}"
|
|
4
|
+
v-bind="chip ? { tag: chipTag, config: chipConfig, status, statusLabel } : {}"
|
|
5
5
|
class="display-avatar"
|
|
6
|
-
:class="
|
|
6
|
+
:class="elementClasses"
|
|
7
|
+
:data-size="size"
|
|
7
8
|
>
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
<span class="display-avatar-surface">
|
|
10
|
+
<slot name="default">
|
|
11
|
+
<NuxtImg
|
|
12
|
+
v-if="src && !imageFailed"
|
|
13
|
+
:src
|
|
14
|
+
:alt="alt ?? ''"
|
|
15
|
+
:width="imageSize"
|
|
16
|
+
:height="imageSize"
|
|
17
|
+
class="display-avatar-image"
|
|
18
|
+
@error="imageFailed = true"
|
|
19
|
+
/>
|
|
20
|
+
<template v-else>
|
|
21
|
+
<span class="display-avatar-fallback" :aria-hidden="alt ? 'true' : undefined">{{ fallback }}</span>
|
|
22
|
+
<span v-if="alt" class="sr-only">{{ alt }}</span>
|
|
23
|
+
</template>
|
|
24
|
+
</slot>
|
|
25
|
+
<slot name="icon"></slot>
|
|
26
|
+
</span>
|
|
16
27
|
</component>
|
|
17
28
|
</template>
|
|
18
29
|
|
|
19
30
|
<script setup lang="ts">
|
|
20
31
|
import DisplayChip from "../display-chip/DisplayChip.vue";
|
|
21
|
-
import type { DisplayChipConfig } from "~/types/components";
|
|
32
|
+
import type { DisplayChipConfig, DisplayChipStatus } from "~/types/components";
|
|
22
33
|
|
|
23
34
|
interface Props {
|
|
24
35
|
as?: string | object;
|
|
@@ -27,6 +38,9 @@ interface Props {
|
|
|
27
38
|
text?: string;
|
|
28
39
|
size?: "xs" | "s" | "md" | "lg" | "xl" | string;
|
|
29
40
|
chip?: boolean | DisplayChipConfig;
|
|
41
|
+
status?: DisplayChipStatus;
|
|
42
|
+
statusLabel?: string;
|
|
43
|
+
imageSize?: number;
|
|
30
44
|
styleClassPassthrough?: string | string[];
|
|
31
45
|
}
|
|
32
46
|
|
|
@@ -37,6 +51,9 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
37
51
|
text: undefined,
|
|
38
52
|
size: "md",
|
|
39
53
|
chip: undefined,
|
|
54
|
+
status: undefined,
|
|
55
|
+
statusLabel: undefined,
|
|
56
|
+
imageSize: 112,
|
|
40
57
|
styleClassPassthrough: () => [],
|
|
41
58
|
});
|
|
42
59
|
|
|
@@ -47,14 +64,26 @@ watch(
|
|
|
47
64
|
() => resetElementClasses(props.styleClassPassthrough)
|
|
48
65
|
);
|
|
49
66
|
|
|
67
|
+
const imageFailed = ref(false);
|
|
68
|
+
|
|
69
|
+
watch(
|
|
70
|
+
() => props.src,
|
|
71
|
+
() => (imageFailed.value = false)
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const graphemes = new Intl.Segmenter(undefined, { granularity: "grapheme" });
|
|
75
|
+
const firstGrapheme = (word: string) => graphemes.segment(word)[Symbol.iterator]().next().value?.segment ?? "";
|
|
76
|
+
|
|
50
77
|
const fallback = computed(
|
|
51
78
|
() =>
|
|
52
|
-
props.text ||
|
|
79
|
+
props.text?.trim() ||
|
|
53
80
|
(props.alt || "")
|
|
54
|
-
.
|
|
55
|
-
.
|
|
81
|
+
.trim()
|
|
82
|
+
.split(/\s+/)
|
|
83
|
+
.filter(Boolean)
|
|
84
|
+
.slice(0, 2)
|
|
85
|
+
.map(firstGrapheme)
|
|
56
86
|
.join("")
|
|
57
|
-
.substring(0, 2)
|
|
58
87
|
);
|
|
59
88
|
|
|
60
89
|
const chipDefaultConfig: DisplayChipConfig = {
|
|
@@ -72,36 +101,52 @@ const chipTag = computed((): "div" | "span" => (props.as === "div" || props.as =
|
|
|
72
101
|
<style lang="css">
|
|
73
102
|
@layer components {
|
|
74
103
|
.display-avatar {
|
|
75
|
-
--
|
|
76
|
-
--
|
|
104
|
+
--_avatar-size: var(--display-avatar-size-md, 4rem);
|
|
105
|
+
--_avatar-font-size: var(--display-avatar-font-size-md, 1.6rem);
|
|
77
106
|
|
|
78
107
|
display: flex;
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
font-size: var(--display-avatar-font-size, var(--_font-size));
|
|
108
|
+
flex-shrink: 0;
|
|
109
|
+
width: var(--display-avatar-size, var(--_avatar-size));
|
|
110
|
+
height: var(--display-avatar-size, var(--_avatar-size));
|
|
111
|
+
font-size: var(--display-avatar-font-size, var(--_avatar-font-size));
|
|
84
112
|
border-radius: var(--display-avatar-border-radius, 50%);
|
|
85
|
-
background-color: var(--display-avatar-background, var(--theme-surface-subtle));
|
|
86
|
-
color: var(--display-avatar-text-colour, var(--theme-text));
|
|
87
113
|
|
|
88
114
|
isolation: isolate;
|
|
89
115
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
116
|
+
/* Fills the root so DisplayChip's per-child cutout mask is sized to the whole avatar. */
|
|
117
|
+
.display-avatar-surface {
|
|
118
|
+
display: flex;
|
|
119
|
+
align-items: center;
|
|
120
|
+
justify-content: center;
|
|
121
|
+
width: 100%;
|
|
122
|
+
height: 100%;
|
|
123
|
+
border-radius: inherit;
|
|
124
|
+
background-color: var(--display-avatar-background, var(--theme-surface-subtle));
|
|
125
|
+
color: var(--display-avatar-text-colour, var(--theme-text));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
&[data-size="xs"] {
|
|
129
|
+
--_avatar-size: var(--display-avatar-size-xs, 2.4rem);
|
|
130
|
+
--_avatar-font-size: var(--display-avatar-font-size-xs, 1.2rem);
|
|
131
|
+
}
|
|
132
|
+
&[data-size="s"] {
|
|
133
|
+
--_avatar-size: var(--display-avatar-size-s, 3.2rem);
|
|
134
|
+
--_avatar-font-size: var(--display-avatar-font-size-s, 1.4rem);
|
|
93
135
|
}
|
|
94
|
-
|
|
95
|
-
--
|
|
96
|
-
--
|
|
136
|
+
&[data-size="lg"] {
|
|
137
|
+
--_avatar-size: var(--display-avatar-size-lg, 4.8rem);
|
|
138
|
+
--_avatar-font-size: var(--display-avatar-font-size-lg, 1.8rem);
|
|
97
139
|
}
|
|
98
|
-
|
|
99
|
-
--
|
|
100
|
-
--
|
|
140
|
+
&[data-size="xl"] {
|
|
141
|
+
--_avatar-size: var(--display-avatar-size-xl, 5.6rem);
|
|
142
|
+
--_avatar-font-size: var(--display-avatar-font-size-xl, 2rem);
|
|
101
143
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
144
|
+
|
|
145
|
+
.display-avatar-fallback {
|
|
146
|
+
min-inline-size: 0;
|
|
147
|
+
max-inline-size: 100%;
|
|
148
|
+
overflow: hidden;
|
|
149
|
+
white-space: nowrap;
|
|
105
150
|
}
|
|
106
151
|
|
|
107
152
|
.display-avatar-image {
|
|
@@ -56,6 +56,21 @@ export default {
|
|
|
56
56
|
category: "Chip Configuration",
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
|
+
status: {
|
|
60
|
+
control: { type: "inline-radio" },
|
|
61
|
+
options: ["offline", "online", "idle", "dnd"],
|
|
62
|
+
description: "Chip status colour (forwarded to DisplayChip)",
|
|
63
|
+
table: {
|
|
64
|
+
category: "Chip Configuration",
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
statusLabel: {
|
|
68
|
+
control: { type: "text" },
|
|
69
|
+
description: "Screen-reader text for the chip status, e.g. 'Online' (forwarded to DisplayChip)",
|
|
70
|
+
table: {
|
|
71
|
+
category: "Chip Configuration",
|
|
72
|
+
},
|
|
73
|
+
},
|
|
59
74
|
// Hide the chip prop from controls since we're using individual controls
|
|
60
75
|
chip: {
|
|
61
76
|
table: {
|
|
@@ -77,10 +92,22 @@ export default {
|
|
|
77
92
|
chipMaskWidth: 4,
|
|
78
93
|
chipOffset: 2,
|
|
79
94
|
chipAngle: 45,
|
|
80
|
-
|
|
95
|
+
status: "online",
|
|
96
|
+
statusLabel: "Online",
|
|
97
|
+
styleClassPassthrough: ["test-storybook--display-avatar"],
|
|
81
98
|
},
|
|
82
99
|
} as Meta<typeof StorybookComponent>;
|
|
83
100
|
|
|
101
|
+
// Links in the canvas open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
102
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
103
|
+
const googleReviewsNote = `
|
|
104
|
+
<p style="${storyNoteStyle}">
|
|
105
|
+
GoogleReviewCard puts an avatar beside each reviewer's name, with their photo or initials and its own avatar colour tokens. See the
|
|
106
|
+
<a href="/?path=/story/organisms-google-reviews--default" target="_top">Google Reviews</a> story, and
|
|
107
|
+
<a href="/?path=/story/organisms-google-reviews--no-avatars" target="_top">No Avatars</a> for the initials fallback.
|
|
108
|
+
</p>
|
|
109
|
+
`;
|
|
110
|
+
|
|
84
111
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
85
112
|
const useChip = (args: any) =>
|
|
86
113
|
computed(() => ({
|
|
@@ -96,12 +123,15 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
96
123
|
return { args, chip: useChip(args) };
|
|
97
124
|
},
|
|
98
125
|
template: `
|
|
99
|
-
|
|
126
|
+
${googleReviewsNote}
|
|
127
|
+
<div style="display: flex; align-items: center; justify-content: center; min-height: 80vh;">
|
|
100
128
|
<StorybookComponent
|
|
101
129
|
:size="args.size"
|
|
102
130
|
:src="args.src"
|
|
103
131
|
:alt="args.alt"
|
|
104
132
|
:chip="chip"
|
|
133
|
+
:status="args.status"
|
|
134
|
+
:status-label="args.statusLabel"
|
|
105
135
|
:style-class-passthrough="args.styleClassPassthrough"
|
|
106
136
|
/>
|
|
107
137
|
</div>
|
|
@@ -116,9 +146,10 @@ export const Initials: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
116
146
|
return { args, chip: useChip(args) };
|
|
117
147
|
},
|
|
118
148
|
template: `
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<StorybookComponent :size="args.size"
|
|
149
|
+
${googleReviewsNote}
|
|
150
|
+
<div style="display: flex; align-items: center; justify-content: center; gap: 1.6rem; min-height: 80vh;">
|
|
151
|
+
<StorybookComponent :size="args.size" :alt="args.alt" :chip="chip" :status="args.status" :status-label="args.statusLabel" :style-class-passthrough="args.styleClassPassthrough" />
|
|
152
|
+
<StorybookComponent :size="args.size" text="?" :chip="chip" :status="args.status" :status-label="args.statusLabel" :style-class-passthrough="args.styleClassPassthrough" />
|
|
122
153
|
</div>
|
|
123
154
|
`,
|
|
124
155
|
});
|
|
@@ -131,7 +162,8 @@ export const Sizes: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
131
162
|
return { args, chip: useChip(args), sizes: ["xs", "s", "md", "lg", "xl"] };
|
|
132
163
|
},
|
|
133
164
|
template: `
|
|
134
|
-
|
|
165
|
+
${googleReviewsNote}
|
|
166
|
+
<div style="display: flex; align-items: center; justify-content: center; gap: 1.6rem; min-height: 80vh;">
|
|
135
167
|
<StorybookComponent
|
|
136
168
|
v-for="size in sizes"
|
|
137
169
|
:key="size"
|
|
@@ -139,6 +171,8 @@ export const Sizes: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
139
171
|
:src="args.src"
|
|
140
172
|
:alt="args.alt"
|
|
141
173
|
:chip="chip"
|
|
174
|
+
:status="args.status"
|
|
175
|
+
:status-label="args.statusLabel"
|
|
142
176
|
:style-class-passthrough="args.styleClassPassthrough"
|
|
143
177
|
/>
|
|
144
178
|
</div>
|
|
@@ -146,3 +180,71 @@ export const Sizes: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
146
180
|
});
|
|
147
181
|
Sizes.args = { src: undefined, alt: "Jane Smith" };
|
|
148
182
|
Sizes.argTypes = { size: { table: { disable: true } } };
|
|
183
|
+
|
|
184
|
+
const stressAvatars = [
|
|
185
|
+
{ key: "long-text", caption: "Long text prop", props: { text: "Administrator account" } },
|
|
186
|
+
{ key: "unbroken", caption: "Unbroken alt", props: { alt: "Pneumonoultramicroscopicsilicovolcanoconiosis" } },
|
|
187
|
+
{ key: "german", caption: "Long German name", props: { alt: "Maximilian-Alexander von Hohenzollern-Sigmaringen" } },
|
|
188
|
+
{ key: "emoji", caption: "Emoji first", props: { alt: "🦄 Unicorn Team" } },
|
|
189
|
+
{ key: "zwj", caption: "ZWJ family emoji", props: { alt: "👩👩👧 Family" } },
|
|
190
|
+
{ key: "flags", caption: "Flag emoji", props: { alt: "🇬🇧 🇫🇷" } },
|
|
191
|
+
{ key: "whitespace", caption: "Extra whitespace", props: { alt: " Jane Smith " } },
|
|
192
|
+
{ key: "text-blank", caption: "Whitespace text prop", props: { alt: "Jane Smith", text: " " } },
|
|
193
|
+
{ key: "single", caption: "Single character", props: { alt: "J" } },
|
|
194
|
+
{ key: "empty", caption: "No alt, text or src", props: {} },
|
|
195
|
+
{ key: "html", caption: "HTML-like alt", props: { alt: "<b>Bold</b> <script>" } },
|
|
196
|
+
{ key: "rtl", caption: "Right-to-left", props: { alt: "محمد علي" }, dir: "rtl" },
|
|
197
|
+
{ key: "wide", caption: "Widest glyphs", props: { text: "WW" } },
|
|
198
|
+
{ key: "broken", caption: "Broken src", props: { src: "/does-not-exist.jpg", alt: "Jane Smith" } },
|
|
199
|
+
{ key: "broken-no-alt", caption: "Broken src, no alt", props: { src: "/does-not-exist.jpg" } },
|
|
200
|
+
{ key: "custom-size", caption: "Unknown size string", props: { alt: "Jane Smith", size: "huge" } },
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
export const StressTest: StoryFn<typeof StorybookComponent> = () => ({
|
|
204
|
+
components: { StorybookComponent },
|
|
205
|
+
setup() {
|
|
206
|
+
return { stressAvatars, sizes: ["xs", "s", "md", "lg", "xl"] };
|
|
207
|
+
},
|
|
208
|
+
template: `
|
|
209
|
+
${googleReviewsNote}
|
|
210
|
+
<div style="display: grid; gap: 4.8rem; padding: 4rem 2rem; font: 1.2rem sans-serif;">
|
|
211
|
+
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(14rem, 1fr)); gap: 3.2rem 2.4rem;">
|
|
212
|
+
<figure
|
|
213
|
+
v-for="avatar in stressAvatars"
|
|
214
|
+
:key="avatar.key"
|
|
215
|
+
:dir="avatar.dir"
|
|
216
|
+
style="margin: 0; display: grid; justify-items: center; align-content: start; gap: 1.2rem; min-inline-size: 0;"
|
|
217
|
+
>
|
|
218
|
+
<StorybookComponent v-bind="avatar.props" :chip="true" status="online" status-label="Online" />
|
|
219
|
+
<figcaption style="text-align: center; overflow-wrap: anywhere;">{{ avatar.caption }}</figcaption>
|
|
220
|
+
</figure>
|
|
221
|
+
</div>
|
|
222
|
+
|
|
223
|
+
<div style="display: flex; align-items: center; gap: 1.6rem;">
|
|
224
|
+
<StorybookComponent v-for="size in sizes" :key="size" :size="size" text="WW" />
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
<div style="display: grid; gap: 1.2rem; inline-size: 20rem; border: 1px dashed currentColor; padding: 0.8rem;">
|
|
228
|
+
<div v-for="size in sizes" :key="size" style="display: flex; align-items: center; gap: 0.8rem;">
|
|
229
|
+
<StorybookComponent :size="size" alt="Jane Smith" />
|
|
230
|
+
<span style="min-inline-size: 0; overflow-wrap: anywhere;">Averyveryverylongunbrokenusername@example.com</span>
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
`,
|
|
235
|
+
});
|
|
236
|
+
StressTest.storyName = "Stress Test (Worst-Case Data)";
|
|
237
|
+
StressTest.parameters = {
|
|
238
|
+
// Fixed showcase: hardcoded values, so Controls would do nothing here.
|
|
239
|
+
controls: { disable: true },
|
|
240
|
+
docs: {
|
|
241
|
+
description: {
|
|
242
|
+
story:
|
|
243
|
+
"Hostile names and sources: long and unbroken text, emoji (including ZWJ and flags) at the start of a name, extra whitespace, a whitespace-only " +
|
|
244
|
+
"text prop, a single character, nothing at all, HTML-like and right-to-left text, the widest Latin glyphs at every size, broken image URLs with and " +
|
|
245
|
+
"without alt, and an unknown size string. The last block puts each size in a narrow flex row next to a long unbroken name. " +
|
|
246
|
+
"Check that initials are whole characters (never a broken half emoji), overlong text is clipped inside the circle rather than spilling out, " +
|
|
247
|
+
"a broken image falls back to initials (or an empty circle with no alt), an unknown size renders at md, and no avatar squashes into an oval in the narrow rows.",
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
};
|
|
@@ -64,14 +64,15 @@ describe("DisplayAvatar", () => {
|
|
|
64
64
|
|
|
65
65
|
// ─── Size ─────────────────────────────────────────────────────────────────
|
|
66
66
|
|
|
67
|
-
it("
|
|
67
|
+
it("sets data-size=\"md\" by default", async () => {
|
|
68
68
|
const wrapper = await mountSuspended(DisplayAvatar);
|
|
69
|
-
expect(wrapper.
|
|
69
|
+
expect(wrapper.attributes("data-size")).toBe("md");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it.each(["xs", "s", "md", "lg", "xl"] as const)("
|
|
72
|
+
it.each(["xs", "s", "md", "lg", "xl"] as const)("sets data-size=\"%s\" when size='%s'", async (size) => {
|
|
73
73
|
const wrapper = await mountSuspended(DisplayAvatar, { props: { size } });
|
|
74
|
-
expect(wrapper.
|
|
74
|
+
expect(wrapper.attributes("data-size")).toBe(size);
|
|
75
|
+
expect(wrapper.classes()).not.toContain(size);
|
|
75
76
|
});
|
|
76
77
|
|
|
77
78
|
// ─── Fallback text ────────────────────────────────────────────────────────
|
|
@@ -80,33 +81,33 @@ describe("DisplayAvatar", () => {
|
|
|
80
81
|
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
81
82
|
props: { alt: "John Doe" },
|
|
82
83
|
});
|
|
83
|
-
expect(wrapper.find(".display-avatar
|
|
84
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("JD");
|
|
84
85
|
});
|
|
85
86
|
|
|
86
87
|
it("caps initials at two characters", async () => {
|
|
87
88
|
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
88
89
|
props: { alt: "Alice Bob Charlie" },
|
|
89
90
|
});
|
|
90
|
-
expect(wrapper.find(".display-avatar
|
|
91
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("AB");
|
|
91
92
|
});
|
|
92
93
|
|
|
93
94
|
it("shows single initial for a single-word alt", async () => {
|
|
94
95
|
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
95
96
|
props: { alt: "Alice" },
|
|
96
97
|
});
|
|
97
|
-
expect(wrapper.find(".display-avatar
|
|
98
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("A");
|
|
98
99
|
});
|
|
99
100
|
|
|
100
101
|
it("shows text prop instead of alt-derived initials when text is set", async () => {
|
|
101
102
|
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
102
103
|
props: { alt: "John Doe", text: "?" },
|
|
103
104
|
});
|
|
104
|
-
expect(wrapper.find(".display-avatar
|
|
105
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("?");
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
it("shows empty fallback when neither src, text, nor alt are set", async () => {
|
|
108
109
|
const wrapper = await mountSuspended(DisplayAvatar);
|
|
109
|
-
expect(wrapper.find(".display-avatar
|
|
110
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("");
|
|
110
111
|
});
|
|
111
112
|
|
|
112
113
|
// ─── Image ────────────────────────────────────────────────────────────────
|
|
@@ -210,13 +211,13 @@ describe("DisplayAvatar", () => {
|
|
|
210
211
|
|
|
211
212
|
it("hides the initials from screen readers and announces the alt instead", async () => {
|
|
212
213
|
const wrapper = await mountSuspended(DisplayAvatar, { props: { alt: "John Doe" } });
|
|
213
|
-
expect(wrapper.find(".display-avatar
|
|
214
|
+
expect(wrapper.find(".display-avatar-fallback").attributes("aria-hidden")).toBe("true");
|
|
214
215
|
expect(wrapper.find(".sr-only").text()).toBe("John Doe");
|
|
215
216
|
});
|
|
216
217
|
|
|
217
218
|
it("leaves text-only fallback readable when there is no alt", async () => {
|
|
218
219
|
const wrapper = await mountSuspended(DisplayAvatar, { props: { text: "?" } });
|
|
219
|
-
expect(wrapper.find(".display-avatar
|
|
220
|
+
expect(wrapper.find(".display-avatar-fallback").attributes("aria-hidden")).toBeUndefined();
|
|
220
221
|
expect(wrapper.find(".sr-only").exists()).toBe(false);
|
|
221
222
|
});
|
|
222
223
|
|
|
@@ -225,11 +226,31 @@ describe("DisplayAvatar", () => {
|
|
|
225
226
|
expect(wrapper.attributes("style-class-passthrough")).toBeUndefined();
|
|
226
227
|
});
|
|
227
228
|
|
|
228
|
-
it("puts display-avatar, size and passthrough classes on the DisplayChip root", async () => {
|
|
229
|
+
it("puts display-avatar, data-size and passthrough classes on the DisplayChip root", async () => {
|
|
229
230
|
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
230
231
|
props: { chip: true, size: "lg", styleClassPassthrough: ["online"] },
|
|
231
232
|
});
|
|
232
|
-
expect(wrapper.classes()).toEqual(expect.arrayContaining(["display-chip", "display-avatar", "
|
|
233
|
+
expect(wrapper.classes()).toEqual(expect.arrayContaining(["display-chip", "display-avatar", "online"]));
|
|
234
|
+
expect(wrapper.attributes("data-size")).toBe("lg");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("forwards status and statusLabel to the chip", async () => {
|
|
238
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
239
|
+
props: { chip: true, alt: "Jane Smith", status: "dnd", statusLabel: "Do not disturb" },
|
|
240
|
+
});
|
|
241
|
+
expect(wrapper.attributes("data-status")).toBe("dnd");
|
|
242
|
+
expect(wrapper.findAll(".sr-only").map((el) => el.text())).toContain("Do not disturb");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("leaves the chip offline when no status is given", async () => {
|
|
246
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { chip: true } });
|
|
247
|
+
expect(wrapper.attributes("data-status")).toBe("offline");
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("does not put status attributes on a plain avatar", async () => {
|
|
251
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { status: "online" } });
|
|
252
|
+
expect(wrapper.attributes("data-status")).toBeUndefined();
|
|
253
|
+
expect(wrapper.attributes("status")).toBeUndefined();
|
|
233
254
|
});
|
|
234
255
|
|
|
235
256
|
it("updates the chip config when the chip prop changes", async () => {
|
|
@@ -237,4 +258,74 @@ describe("DisplayAvatar", () => {
|
|
|
237
258
|
await wrapper.setProps({ chip: { size: "20px", maskWidth: "2px", offset: "0px", angle: "0deg" } });
|
|
238
259
|
expect(wrapper.findComponent(DisplayChip).props("config")).toMatchObject({ size: "20px" });
|
|
239
260
|
});
|
|
261
|
+
|
|
262
|
+
// ─── Stress test 2026-10-14 ────────────────────────────────────────────────
|
|
263
|
+
|
|
264
|
+
const initialsFor = async (alt: string) => {
|
|
265
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { alt } });
|
|
266
|
+
return wrapper.find(".display-avatar-fallback").text();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
it("takes whole emoji as initials, not half a surrogate pair", async () => {
|
|
270
|
+
expect(await initialsFor("🦄 Unicorn")).toBe("🦄U");
|
|
271
|
+
expect(await initialsFor("👩👩👧 Family")).toBe("👩👩👧F");
|
|
272
|
+
expect(await initialsFor("🇬🇧 Team")).toBe("🇬🇧T");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
it("ignores leading, trailing and repeated whitespace in alt", async () => {
|
|
276
|
+
expect(await initialsFor(" Jane Smith ")).toBe("JS");
|
|
277
|
+
expect(await initialsFor("Jane\tSmith")).toBe("JS");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("falls back to alt initials when text is whitespace only", async () => {
|
|
281
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { alt: "Jane Smith", text: " " } });
|
|
282
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("JS");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("renders HTML-like alt as text", async () => {
|
|
286
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { alt: "<b>Bold</b> <script>" } });
|
|
287
|
+
expect(wrapper.find("b").exists()).toBe(false);
|
|
288
|
+
expect(wrapper.find(".sr-only").text()).toBe("<b>Bold</b> <script>");
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("passes a numeric width and height to the image", async () => {
|
|
292
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { src: "/avatar.jpg" } });
|
|
293
|
+
const img = wrapper.find(".display-avatar-image");
|
|
294
|
+
expect(img.attributes("width")).toBe("112");
|
|
295
|
+
expect(img.attributes("height")).toBe("112");
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
it("uses imageSize for the image's width and height", async () => {
|
|
299
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { src: "/avatar.jpg", imageSize: 240 } });
|
|
300
|
+
expect(wrapper.find(".display-avatar-image").attributes("width")).toBe("240");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("shows the initials fallback when the image fails to load", async () => {
|
|
304
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { src: "/broken.jpg", alt: "Jane Smith" } });
|
|
305
|
+
await wrapper.find(".display-avatar-image").trigger("error");
|
|
306
|
+
expect(wrapper.find(".display-avatar-image").exists()).toBe(false);
|
|
307
|
+
expect(wrapper.find(".display-avatar-fallback").text()).toBe("JS");
|
|
308
|
+
expect(wrapper.find(".sr-only").text()).toBe("Jane Smith");
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it("tries the image again when src changes after a failure", async () => {
|
|
312
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { src: "/broken.jpg", alt: "Jane Smith" } });
|
|
313
|
+
await wrapper.find(".display-avatar-image").trigger("error");
|
|
314
|
+
await wrapper.setProps({ src: "/working.jpg" });
|
|
315
|
+
expect(wrapper.find(".display-avatar-image").exists()).toBe(true);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("wraps all content in one surface so the chip's per-child mask covers the whole avatar", async () => {
|
|
319
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
320
|
+
props: { alt: "Jane Smith", chip: true },
|
|
321
|
+
slots: { icon: "<span class='status-icon'>●</span>" },
|
|
322
|
+
});
|
|
323
|
+
const masked = Array.from((wrapper.element as HTMLElement).children).filter(
|
|
324
|
+
(el) => !el.matches(".display-chip-icon, .display-chip-label, .sr-only")
|
|
325
|
+
);
|
|
326
|
+
expect(masked).toHaveLength(1);
|
|
327
|
+
expect(masked[0]!.classList.contains("display-avatar-surface")).toBe(true);
|
|
328
|
+
expect(masked[0]!.querySelector(".display-avatar-fallback")?.textContent).toBe("JS");
|
|
329
|
+
expect(masked[0]!.querySelector(".status-icon")).not.toBeNull();
|
|
330
|
+
});
|
|
240
331
|
});
|
package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
|
2
2
|
|
|
3
|
-
exports[`DisplayAvatar > renders correct HTML structure (default) 1`] = `"<span class="display-avatar md"><span aria-hidden="true">JD</span><span class="sr-only">John Doe</span></span>"`;
|
|
3
|
+
exports[`DisplayAvatar > renders correct HTML structure (default) 1`] = `"<span class="display-avatar" data-size="md"><span class="display-avatar-surface"><span class="display-avatar-fallback" aria-hidden="true">JD</span><span class="sr-only">John Doe</span></span></span>"`;
|
|
4
4
|
|
|
5
5
|
exports[`DisplayAvatar > renders correct HTML structure (with chip) 1`] = `
|
|
6
|
-
"<span class="display-chip
|
|
6
|
+
"<span class="display-chip display-avatar" data-shape="circle" data-status="offline" style="--_chip-size: 12px; --_chip-mask-width: 4px; --_chip-offset: 0px; --_chip-angle: 90deg;" data-size="md"><span class="display-avatar-surface"><span class="display-avatar-fallback" aria-hidden="true">JD</span><span class="sr-only">John Doe</span></span>
|
|
7
|
+
<!--v-if-->
|
|
7
8
|
<!--v-if-->
|
|
8
9
|
<!--v-if--></span>"
|
|
9
10
|
`;
|
|
10
11
|
|
|
11
|
-
exports[`DisplayAvatar > renders correct HTML structure (with src) 1`] = `"<span class="display-avatar lg"><img data-nuxt-img="" srcset="/_ipx/
|
|
12
|
+
exports[`DisplayAvatar > renders correct HTML structure (with src) 1`] = `"<span class="display-avatar" data-size="lg"><span class="display-avatar-surface"><img width="112" height="112" data-nuxt-img="" srcset="/_ipx/s_112x112/avatar.jpg 1x, /_ipx/s_224x224/avatar.jpg 2x" alt="John Doe" class="display-avatar-image" src="/_ipx/s_112x112/avatar.jpg"></span></span>"`;
|
|
@@ -2,19 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
## Public token API
|
|
4
4
|
|
|
5
|
+
### Geometry
|
|
6
|
+
|
|
5
7
|
| Token | Default | Controls |
|
|
6
8
|
|---|---|---|
|
|
7
|
-
| `--display-chip-
|
|
8
|
-
| `--display-chip-
|
|
9
|
-
| `--display-chip-
|
|
10
|
-
| `--display-chip-
|
|
9
|
+
| `--display-chip-size` | `1.2rem` | Dot diameter. Negative values clamp to `0` (no dot) |
|
|
10
|
+
| `--display-chip-mask-width` | `0.4rem` | Width of the cutout ring around the dot. Negative values clamp to `0` |
|
|
11
|
+
| `--display-chip-offset` | `0rem` | Extra distance from the host's edge (negative pulls the dot inwards) |
|
|
12
|
+
| `--display-chip-angle` | `90deg` | Position around the host, clockwise from the top |
|
|
13
|
+
|
|
14
|
+
A field set in the `config` prop wins over its token for that chip. Fields left out of `config` fall
|
|
15
|
+
back to the token, so `:config="{ label: '5' }"` gets the default geometry.
|
|
16
|
+
|
|
17
|
+
> Changed 2026-10-08: geometry tokens are new. Before, geometry came only from `config`, and a
|
|
18
|
+
> `config` missing any of `size`/`maskWidth`/`offset`/`angle` broke the dot and cutout entirely.
|
|
19
|
+
|
|
20
|
+
### Colour
|
|
21
|
+
|
|
22
|
+
| Token | Default | Controls |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `--display-chip-colour-offline` | `var(--status-neutral)` | Dot colour for `status="offline"` (the default) |
|
|
25
|
+
| `--display-chip-colour-online` | `var(--status-success)` | Dot colour for `status="online"` |
|
|
26
|
+
| `--display-chip-colour-idle` | `var(--status-warning)` | Dot colour for `status="idle"` |
|
|
27
|
+
| `--display-chip-colour-dnd` | `var(--status-danger)` | Dot colour for `status="dnd"` |
|
|
11
28
|
| `--display-chip-text-colour` | `black` | Icon and label colour on the dot |
|
|
12
29
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
`--
|
|
17
|
-
of these are public API.
|
|
30
|
+
Private tokens (not public API): `--_chip-size`, `--_chip-mask-width`, `--_chip-offset` and
|
|
31
|
+
`--_chip-angle` (written inline from `config` or the geometry tokens), `--_size`, and the position
|
|
32
|
+
maths (`--_mask-diameter`, `--_position-x`, `--_position-y`, `--_offset`, `--_circle-x`,
|
|
33
|
+
`--_circle-y`), plus the resolved `--_dot-colour` and the label's `--_font-size-adjust`.
|
|
18
34
|
|
|
19
35
|
> Changed 2026-09-27: the status colours were unprefixed `--color-offline`, `--color-online`,
|
|
20
36
|
> `--color-idle` and `--color-dnd`, and the geometry and maths vars were unprefixed too
|
|
@@ -22,20 +38,37 @@ of these are public API.
|
|
|
22
38
|
> consumer variable of the same name. The icon is now sized with `font-size` (its old
|
|
23
39
|
> `width`/`height` were overridden by `@nuxt/icon`), and the icon/label colour is a token instead
|
|
24
40
|
> of a hardcoded `black`.
|
|
41
|
+
>
|
|
42
|
+
> Changed 2026-10-07: the status colours default to the global `--status-*` tokens
|
|
43
|
+
> (`03.theming/_status.css`) instead of hardcoded neon `rgb()` values and `slategrey`, so they
|
|
44
|
+
> match `DisplayPill`, `SelectMenu` and anything else reading the shared set. To keep the old
|
|
45
|
+
> look, set the four `--display-chip-colour-*` tokens to the previous values. To restyle every
|
|
46
|
+
> status across the library, override `--status-*` instead. See
|
|
47
|
+
> `.claude/skills/theming-status-tokens.md`.
|
|
25
48
|
|
|
26
49
|
---
|
|
27
50
|
|
|
28
51
|
## State hooks
|
|
29
52
|
|
|
30
|
-
|
|
|
31
|
-
|
|
32
|
-
| `.display-chip
|
|
33
|
-
|
|
|
53
|
+
| Hook | Element | When |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| `[data-shape="circle\|square"]` | `.display-chip` (root) | The `shape` prop (must match the slotted content's shape) |
|
|
56
|
+
| `[data-status="offline\|online\|idle\|dnd"]` | `.display-chip` (root) | The `status` prop (default `offline`) |
|
|
57
|
+
| `[data-length="1\|2\|3"]` | `.display-chip-label` | Label length in characters (emoji count as one), for font-size stepping |
|
|
34
58
|
|
|
35
|
-
Inner elements: the dot is `.display-chip::after`; `.chip-icon` and `.chip-label`
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
Inner elements: the dot is `.display-chip::after`; `.display-chip-icon` and `.display-chip-label`
|
|
60
|
+
sit on top of it. The `statusLabel` text is a `.sr-only` span. Every other direct child gets the
|
|
61
|
+
cutout `mask-image`. The mask is positioned in percentages of each child's own box, so the slot
|
|
62
|
+
should be one element that fills the chip and carries any background: a small child (a bare text
|
|
63
|
+
span) gets the notch cut into its own content, and a background on the root is never masked.
|
|
64
|
+
`DisplayAvatar` uses a full-size `.display-avatar-surface` for this.
|
|
38
65
|
|
|
66
|
+
> Changed 2026-10-08: shape and status are `data-shape`/`data-status` attributes instead of the
|
|
67
|
+
> `.circle`/`.square` and `.online`/`.idle`/`.dnd` classes, so they can't collide with a
|
|
68
|
+
> consumer's own classes of the same name. Status is now the `status` prop rather than a class
|
|
69
|
+
> passed through `style-class-passthrough`. `.chip-icon` → `.display-chip-icon`, `.chip-label` →
|
|
70
|
+
> `.display-chip-label`, and its `.length-N` classes → `data-length="N"`.
|
|
71
|
+
>
|
|
39
72
|
> Changed 2026-09-27: root class renamed from `.display-chip-core`.
|
|
40
73
|
|
|
41
74
|
---
|
|
@@ -48,10 +81,15 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
48
81
|
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the component need
|
|
49
82
|
`:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
50
83
|
|
|
84
|
+
**Caveat:** a geometry field set in `config` is written inline on the chip, so it beats the
|
|
85
|
+
matching `--display-chip-*` geometry token from any ancestor. Leave the field out of `config` to
|
|
86
|
+
let the token apply.
|
|
87
|
+
|
|
51
88
|
---
|
|
52
89
|
|
|
53
90
|
## Class passthrough
|
|
54
91
|
|
|
55
|
-
`style-class-passthrough` adds classes to the root `.display-chip`.
|
|
56
|
-
|
|
57
|
-
|
|
92
|
+
`style-class-passthrough` adds classes to the root `.display-chip`. Reactive after mount.
|
|
93
|
+
`DisplayAvatar` with `chip` renders this component as its root, so the same applies there. Because the
|
|
94
|
+
root element is shared, a wrapping component must not reuse this component's private token names
|
|
95
|
+
(`--_size` especially) for its own values.
|