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,5 +1,5 @@
|
|
|
1
1
|
<template>
|
|
2
|
-
<div class="alert-content" :data-theme="theme">
|
|
2
|
+
<div class="alert-content" :class="[elementClasses]" :data-theme="theme">
|
|
3
3
|
<AlertContentInner
|
|
4
4
|
:theme="theme"
|
|
5
5
|
:custom-icon="customIcon"
|
|
@@ -41,19 +41,28 @@ interface Props {
|
|
|
41
41
|
dismissible?: boolean;
|
|
42
42
|
contentId?: string;
|
|
43
43
|
ariaLive?: "polite" | "assertive" | "off";
|
|
44
|
+
styleClassPassthrough?: string | string[];
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
withDefaults(defineProps<Props>(), {
|
|
47
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
47
48
|
customIcon: undefined,
|
|
48
49
|
showIcon: true,
|
|
49
50
|
dismissible: false,
|
|
50
51
|
contentId: undefined,
|
|
51
52
|
ariaLive: undefined,
|
|
53
|
+
styleClassPassthrough: () => [],
|
|
52
54
|
});
|
|
53
55
|
|
|
54
56
|
const emit = defineEmits<{ dismiss: [] }>();
|
|
55
57
|
|
|
56
58
|
const slots = useSlots();
|
|
59
|
+
|
|
60
|
+
const { elementClasses, resetElementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
|
|
61
|
+
|
|
62
|
+
watch(
|
|
63
|
+
() => props.styleClassPassthrough,
|
|
64
|
+
() => resetElementClasses(props.styleClassPassthrough)
|
|
65
|
+
);
|
|
57
66
|
</script>
|
|
58
67
|
|
|
59
68
|
<style lang="css">
|
|
@@ -89,6 +89,7 @@ const dismissIcon = computed(() => appConfig.srcdev?.alertContent?.dismissIcon ?
|
|
|
89
89
|
border-start-start-radius: var(--alert-content-border-radius-start, 0.8rem);
|
|
90
90
|
border-end-start-radius: var(--alert-content-border-radius-start, 0.8rem);
|
|
91
91
|
padding: var(--alert-content-padding, 1.2rem 1.5rem);
|
|
92
|
+
min-inline-size: 0;
|
|
92
93
|
overflow: hidden;
|
|
93
94
|
|
|
94
95
|
.alert-content-icon {
|
|
@@ -124,6 +125,12 @@ const dismissIcon = computed(() => appConfig.srcdev?.alertContent?.dismissIcon ?
|
|
|
124
125
|
font-weight: 600;
|
|
125
126
|
line-height: 1.2;
|
|
126
127
|
margin: 0;
|
|
128
|
+
display: -webkit-box;
|
|
129
|
+
-webkit-box-orient: vertical;
|
|
130
|
+
overflow: hidden;
|
|
131
|
+
overflow-wrap: anywhere;
|
|
132
|
+
-webkit-line-clamp: var(--alert-content-title-line-clamp, none);
|
|
133
|
+
line-clamp: var(--alert-content-title-line-clamp, none);
|
|
127
134
|
}
|
|
128
135
|
|
|
129
136
|
.alert-content-text {
|
|
@@ -132,6 +139,12 @@ const dismissIcon = computed(() => appConfig.srcdev?.alertContent?.dismissIcon ?
|
|
|
132
139
|
line-height: 1.4;
|
|
133
140
|
margin: 0;
|
|
134
141
|
opacity: 0.9;
|
|
142
|
+
display: -webkit-box;
|
|
143
|
+
-webkit-box-orient: vertical;
|
|
144
|
+
overflow: hidden;
|
|
145
|
+
overflow-wrap: anywhere;
|
|
146
|
+
-webkit-line-clamp: var(--alert-content-text-line-clamp, none);
|
|
147
|
+
line-clamp: var(--alert-content-text-line-clamp, none);
|
|
135
148
|
}
|
|
136
149
|
}
|
|
137
150
|
|
|
@@ -141,6 +154,12 @@ const dismissIcon = computed(() => appConfig.srcdev?.alertContent?.dismissIcon ?
|
|
|
141
154
|
align-items: center;
|
|
142
155
|
justify-content: var(--alert-content-actions-justify, flex-end);
|
|
143
156
|
gap: var(--alert-content-actions-gap, 0.8rem);
|
|
157
|
+
--input-button-text-white-space: normal;
|
|
158
|
+
|
|
159
|
+
& > * {
|
|
160
|
+
min-inline-size: 0;
|
|
161
|
+
max-inline-size: 100%;
|
|
162
|
+
}
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
.alert-content-dismiss {
|
|
@@ -28,6 +28,8 @@ Covers `AlertContent` and its inner `AlertContentInner`. The inner tokens also a
|
|
|
28
28
|
| `--alert-content-body-gap` | `0.4rem` | Gap between title and message |
|
|
29
29
|
| `--alert-content-title-font-size` | `var(--step-4)` | Title size |
|
|
30
30
|
| `--alert-content-text-font-size` | `var(--step-3)` | Message size |
|
|
31
|
+
| `--alert-content-title-line-clamp` | `none` | Max title lines before an ellipsis (`1` = single-line ellipsis, `none` = show everything) |
|
|
32
|
+
| `--alert-content-text-line-clamp` | `none` | Max message lines before an ellipsis. Leave it at `none` for messages people must read in full (errors, consent text): clamped text is still read by screen readers but hidden from sighted users |
|
|
31
33
|
| `--alert-content-actions-spacing` | `1.2rem` | Space between the message and the actions row |
|
|
32
34
|
| `--alert-content-actions-gap` | `0.8rem` | Gap between action controls |
|
|
33
35
|
| `--alert-content-actions-justify` | `flex-end` | Horizontal alignment of the actions row (`justify-content`) |
|
|
@@ -42,6 +44,9 @@ Covers `AlertContent` and its inner `AlertContentInner`. The inner tokens also a
|
|
|
42
44
|
| `--alert-content-dismiss-colour-hover` | `var(--theme-on-surface)` | Icon colour on hover/focus |
|
|
43
45
|
| `--alert-content-dismiss-ring` | `var(--theme-ring)` | Focus/hover outline colour |
|
|
44
46
|
|
|
47
|
+
> **Changed 2026-10-08**: added the two line-clamp tokens. The title and message now wrap long
|
|
48
|
+
> unbroken strings (`overflow-wrap: anywhere`) instead of being clipped at the alert's edge.
|
|
49
|
+
|
|
45
50
|
> **Changed 2026-09-27**: every value above except `--alert-content-inner-background` was hardcoded
|
|
46
51
|
> (or read a `--theme-*` token directly, CLAUDE.md pitfall #14). Defaults are unchanged apart from
|
|
47
52
|
> the px radii and stripe width, which are now the same sizes in rem. The icon and dismiss colours now
|
|
@@ -66,8 +71,8 @@ radius tokens once for the four corners.
|
|
|
66
71
|
|
|
67
72
|
## Local overrides
|
|
68
73
|
|
|
69
|
-
Set the tokens above on an element you own (a page or section class, or a
|
|
70
|
-
component, which
|
|
74
|
+
Set the tokens above on an element you own (a page or section class, or a `:style-class-passthrough`
|
|
75
|
+
class on the component, which lands on the root `.alert-content`): they inherit down into the component.
|
|
71
76
|
Keep the block **unlayered** (no `@layer` wrapper) so it beats the library's `@layer components`. If
|
|
72
77
|
your own file uses `<style scoped>`, tokens set on your element still work, but selectors that reach
|
|
73
78
|
inside the component need `:deep()`. Patterns and examples:
|
|
@@ -78,7 +83,15 @@ don't reach it: the alert always uses its own `theme` prop's palette. Override t
|
|
|
78
83
|
tokens instead. When it's rendered by `DisplayToast`/`DisplayToastProvider` it's teleported to
|
|
79
84
|
`<body>`, so set tokens globally or through the toast's own hooks rather than on a page wrapper.
|
|
80
85
|
|
|
86
|
+
**Caveat:** `.alert-content-actions` sets `--input-button-text-white-space: normal`, so an
|
|
87
|
+
`InputButton` in the `#actions` slot wraps a long label instead of spilling out of the alert. A value
|
|
88
|
+
for that token set on an ancestor doesn't reach buttons inside the actions row; set it on the button
|
|
89
|
+
itself (its own `style-class-passthrough` class) if you need `nowrap` there.
|
|
90
|
+
|
|
81
91
|
## Class passthrough
|
|
82
92
|
|
|
83
|
-
|
|
84
|
-
|
|
93
|
+
`style-class-passthrough` adds classes to the root `.alert-content`, so it's the hook for setting tokens
|
|
94
|
+
on one instance. A plain `class` attribute lands there too (single root). Components that render
|
|
95
|
+
`AlertContent` for you (`DisplayToast`, `DisplayPrompt`, `CookieConsentBanner`) don't forward it.
|
|
96
|
+
|
|
97
|
+
> **Changed 2026-10-08**: added the prop. Before this, a plain `class` was the only option.
|
|
@@ -1,11 +1,34 @@
|
|
|
1
|
+
import { computed, ref } from "vue";
|
|
1
2
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
2
3
|
import AlertContent from "../AlertContent.vue";
|
|
3
4
|
import InputButton from "../../../05.forms/input-button/InputButton.vue";
|
|
5
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
4
7
|
|
|
5
|
-
|
|
8
|
+
type LineClamp = "none" | "1" | "2" | "3" | "4";
|
|
9
|
+
|
|
10
|
+
// Story-only args: drive CSS tokens, not AlertContent props. See their argTypes entries.
|
|
11
|
+
type StoryArgs = InstanceType<typeof AlertContent>["$props"] & {
|
|
12
|
+
titleLineClamp: LineClamp;
|
|
13
|
+
textLineClamp: LineClamp;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const lineClampArgType = (token: string, example: string) => ({
|
|
17
|
+
control: "select" as const,
|
|
18
|
+
options: ["none", "1", "2", "3", "4"] satisfies LineClamp[],
|
|
19
|
+
description:
|
|
20
|
+
`**Story control, not a prop.** Sets the \`${token}\` CSS token on a wrapper so you can try it here. ` +
|
|
21
|
+
`To use it in an app, set the token in your own CSS, e.g. \`.site-notice { ${token}: ${example}; }\`. ` +
|
|
22
|
+
"`1` is single-line ellipsis, `none` (the default) shows everything. Don't clamp messages people need to read in full.",
|
|
23
|
+
table: { category: "CSS tokens (story only, set in your CSS)", defaultValue: { summary: "none" } },
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const meta: Meta<StoryArgs> = {
|
|
6
27
|
title: "Molecules/AlertContent",
|
|
7
28
|
component: AlertContent,
|
|
8
29
|
argTypes: {
|
|
30
|
+
titleLineClamp: lineClampArgType("--alert-content-title-line-clamp", "1"),
|
|
31
|
+
textLineClamp: lineClampArgType("--alert-content-text-line-clamp", "3"),
|
|
9
32
|
theme: {
|
|
10
33
|
control: { type: "inline-radio" },
|
|
11
34
|
options: ["info", "success", "warning", "error"],
|
|
@@ -27,6 +50,11 @@ const meta: Meta<typeof AlertContent> = {
|
|
|
27
50
|
description: "Show a close button (emits 'dismiss' on click)",
|
|
28
51
|
table: { category: "Behaviour" },
|
|
29
52
|
},
|
|
53
|
+
styleClassPassthrough: {
|
|
54
|
+
control: "object",
|
|
55
|
+
description: "Additional CSS classes applied to the root element",
|
|
56
|
+
table: { category: "Styling" },
|
|
57
|
+
},
|
|
30
58
|
contentId: { table: { disable: true } },
|
|
31
59
|
ariaLive: { table: { disable: true } },
|
|
32
60
|
},
|
|
@@ -34,22 +62,53 @@ const meta: Meta<typeof AlertContent> = {
|
|
|
34
62
|
theme: "info",
|
|
35
63
|
showIcon: true,
|
|
36
64
|
dismissible: false,
|
|
65
|
+
titleLineClamp: "none",
|
|
66
|
+
textLineClamp: "none",
|
|
37
67
|
},
|
|
38
68
|
};
|
|
39
69
|
|
|
40
70
|
export default meta;
|
|
41
|
-
type Story = StoryObj<
|
|
71
|
+
type Story = StoryObj<StoryArgs>;
|
|
72
|
+
|
|
73
|
+
// Links open in the Storybook manager (target="_top"), not inside the preview iframe.
|
|
74
|
+
const storyNoteStyle = "margin: 0 0 1.6rem; font-size: 1.4rem;";
|
|
75
|
+
const toastNote = `
|
|
76
|
+
<p style="${storyNoteStyle}">
|
|
77
|
+
To see the dismiss action and auto-dismiss in context, open the
|
|
78
|
+
<a href="/?path=/story/molecules-displaytoast--default" target="_top">DisplayToast</a> and
|
|
79
|
+
<a href="/?path=/story/molecules-displaytoastprovider--default" target="_top">DisplayToastProvider</a> stories.
|
|
80
|
+
</p>
|
|
81
|
+
`;
|
|
82
|
+
const cookieNote = `
|
|
83
|
+
<p style="${storyNoteStyle}">
|
|
84
|
+
For the #actions slot wired up to real accept and reject handlers, open the
|
|
85
|
+
<a href="/?path=/story/molecules-cookieconsentbanner--default" target="_top">CookieConsentBanner</a> story.
|
|
86
|
+
</p>
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
function useStorySetup(args: StoryArgs) {
|
|
90
|
+
const componentArgs = computed(() => {
|
|
91
|
+
const { titleLineClamp: _titleLineClamp, textLineClamp: _textLineClamp, ...rest } = args;
|
|
92
|
+
return rest;
|
|
93
|
+
});
|
|
94
|
+
const tokenStyles = computed(() => ({
|
|
95
|
+
"--alert-content-title-line-clamp": args.titleLineClamp,
|
|
96
|
+
"--alert-content-text-line-clamp": args.textLineClamp,
|
|
97
|
+
}));
|
|
98
|
+
return { args, componentArgs, tokenStyles };
|
|
99
|
+
}
|
|
42
100
|
|
|
43
101
|
/** Default — info theme with title and body content. */
|
|
44
102
|
export const Default: Story = {
|
|
45
103
|
render: (args) => ({
|
|
46
104
|
components: { AlertContent },
|
|
47
105
|
setup() {
|
|
48
|
-
return
|
|
106
|
+
return useStorySetup(args);
|
|
49
107
|
},
|
|
50
108
|
template: `
|
|
51
|
-
<div :data-theme="args.theme" style="max-width: 600px; padding: 2rem;">
|
|
52
|
-
|
|
109
|
+
<div :data-theme="args.theme" :style="tokenStyles" style="max-width: 600px; padding: 2rem;">
|
|
110
|
+
${toastNote}
|
|
111
|
+
<AlertContent v-bind="componentArgs">
|
|
53
112
|
<template #title>Alert title</template>
|
|
54
113
|
<template #content>This is the alert body. It describes what happened and what the user should do next.</template>
|
|
55
114
|
</AlertContent>
|
|
@@ -64,11 +123,12 @@ export const Dismissible: Story = {
|
|
|
64
123
|
render: (args) => ({
|
|
65
124
|
components: { AlertContent },
|
|
66
125
|
setup() {
|
|
67
|
-
return
|
|
126
|
+
return useStorySetup(args);
|
|
68
127
|
},
|
|
69
128
|
template: `
|
|
70
|
-
<div :data-theme="args.theme" style="max-width: 600px; padding: 2rem;">
|
|
71
|
-
|
|
129
|
+
<div :data-theme="args.theme" :style="tokenStyles" style="max-width: 600px; padding: 2rem;">
|
|
130
|
+
${toastNote}
|
|
131
|
+
<AlertContent v-bind="componentArgs">
|
|
72
132
|
<template #title>Dismissible alert</template>
|
|
73
133
|
<template #content>Click the close button to emit the dismiss event.</template>
|
|
74
134
|
</AlertContent>
|
|
@@ -84,11 +144,12 @@ export const TitleOnly: Story = {
|
|
|
84
144
|
render: (args) => ({
|
|
85
145
|
components: { AlertContent },
|
|
86
146
|
setup() {
|
|
87
|
-
return
|
|
147
|
+
return useStorySetup(args);
|
|
88
148
|
},
|
|
89
149
|
template: `
|
|
90
|
-
<div :data-theme="args.theme" style="max-width: 600px; padding: 2rem;">
|
|
91
|
-
|
|
150
|
+
<div :data-theme="args.theme" :style="tokenStyles" style="max-width: 600px; padding: 2rem;">
|
|
151
|
+
${toastNote}
|
|
152
|
+
<AlertContent v-bind="componentArgs">
|
|
92
153
|
<template #title>Your changes have been saved.</template>
|
|
93
154
|
</AlertContent>
|
|
94
155
|
</div>
|
|
@@ -103,11 +164,12 @@ export const CustomIcon: Story = {
|
|
|
103
164
|
render: (args) => ({
|
|
104
165
|
components: { AlertContent },
|
|
105
166
|
setup() {
|
|
106
|
-
return
|
|
167
|
+
return useStorySetup(args);
|
|
107
168
|
},
|
|
108
169
|
template: `
|
|
109
|
-
<div :data-theme="args.theme" style="max-width: 600px; padding: 2rem;">
|
|
110
|
-
|
|
170
|
+
<div :data-theme="args.theme" :style="tokenStyles" style="max-width: 600px; padding: 2rem;">
|
|
171
|
+
${toastNote}
|
|
172
|
+
<AlertContent v-bind="componentArgs">
|
|
111
173
|
<template #title>Custom icon via prop</template>
|
|
112
174
|
<template #content>The icon is overridden using the customIcon prop.</template>
|
|
113
175
|
</AlertContent>
|
|
@@ -126,6 +188,7 @@ export const AllThemes: Story = {
|
|
|
126
188
|
},
|
|
127
189
|
template: `
|
|
128
190
|
<div style="display: flex; flex-direction: column; gap: 1.6rem; max-width: 600px; padding: 2rem;">
|
|
191
|
+
${toastNote}
|
|
129
192
|
<div v-for="theme in themes" :key="theme" :data-theme="theme">
|
|
130
193
|
<AlertContent :theme="theme" :dismissible="true">
|
|
131
194
|
<template #title>{{ theme.charAt(0).toUpperCase() + theme.slice(1) }}</template>
|
|
@@ -144,11 +207,12 @@ export const WithActions: Story = {
|
|
|
144
207
|
render: (args) => ({
|
|
145
208
|
components: { AlertContent, InputButton },
|
|
146
209
|
setup() {
|
|
147
|
-
return
|
|
210
|
+
return useStorySetup(args);
|
|
148
211
|
},
|
|
149
212
|
template: `
|
|
150
|
-
<div :data-theme="args.theme" style="max-width: 600px; padding: 2rem;">
|
|
151
|
-
|
|
213
|
+
<div :data-theme="args.theme" :style="tokenStyles" style="max-width: 600px; padding: 2rem;">
|
|
214
|
+
${cookieNote}
|
|
215
|
+
<AlertContent v-bind="componentArgs">
|
|
152
216
|
<template #title>Cookies</template>
|
|
153
217
|
<template #content>This site uses cookies to understand how it's used. You can accept or reject them.</template>
|
|
154
218
|
<template #actions>
|
|
@@ -163,8 +227,127 @@ export const WithActions: Story = {
|
|
|
163
227
|
docs: {
|
|
164
228
|
description: {
|
|
165
229
|
story:
|
|
230
|
+
"For the slot wired to real handlers, see the [CookieConsentBanner](?path=/story/molecules-cookieconsentbanner--default) story. " +
|
|
166
231
|
"The #actions slot renders a wrapping row under the message, outside the aria-live/contentId body so the buttons aren't announced or pulled into aria-describedby. Actions are right-aligned by default (--alert-content-actions-justify). The cookie icon comes from customIcon; set showIcon to false to drop the icon column.",
|
|
167
232
|
},
|
|
168
233
|
},
|
|
169
234
|
},
|
|
170
235
|
};
|
|
236
|
+
|
|
237
|
+
const stressAlerts = [
|
|
238
|
+
{
|
|
239
|
+
key: "german",
|
|
240
|
+
theme: "error",
|
|
241
|
+
dismissible: true,
|
|
242
|
+
title: "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft",
|
|
243
|
+
content:
|
|
244
|
+
"Ihre Zahlung konnte leider nicht verarbeitet werden. Bitte überprüfen Sie Ihre Kartendaten und versuchen Sie es erneut, oder wenden Sie sich an Ihre Bank. https://example.com/zahlungen/fehler/a-very-long-path-segment-without-any-spaces-at-all-in-it",
|
|
245
|
+
actions: ["Zahlungsinformationenaktualisierung", "Kundendienstmitarbeiterkontaktaufnahme"],
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
key: "emoji-html",
|
|
249
|
+
theme: "warning",
|
|
250
|
+
dismissible: true,
|
|
251
|
+
title: "🚨🔥 Heads up <script>alert('xss')</script>",
|
|
252
|
+
content: "<b>Not bold</b> & <img src=x onerror=alert(1)> should all render as plain text. 👍🏽👨👩👧👦",
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
key: "rtl",
|
|
256
|
+
theme: "info",
|
|
257
|
+
dir: "rtl",
|
|
258
|
+
dismissible: true,
|
|
259
|
+
title: "تنبيه مهم بخصوص حسابك",
|
|
260
|
+
content: "تم تحديث شروط الخدمة. يرجى مراجعة التغييرات قبل المتابعة لاستخدام الموقع.",
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
key: "single-char",
|
|
264
|
+
theme: "success",
|
|
265
|
+
title: "Ok",
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
key: "broken-icon-empty-title",
|
|
269
|
+
theme: "info",
|
|
270
|
+
customIcon: "not-a-real-set:missing-icon",
|
|
271
|
+
title: "",
|
|
272
|
+
content: "Broken customIcon name and an empty title slot.",
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
key: "content-only-no-icon",
|
|
276
|
+
theme: "success",
|
|
277
|
+
showIcon: false,
|
|
278
|
+
content: "x",
|
|
279
|
+
},
|
|
280
|
+
];
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Stress test — hostile content across several alerts, inside the CanvasSwitcher so it can be
|
|
284
|
+
* checked at every width.
|
|
285
|
+
*/
|
|
286
|
+
export const StressTest: Story = {
|
|
287
|
+
name: "Stress Test (Worst-Case Data)",
|
|
288
|
+
args: { titleLineClamp: "none", textLineClamp: "none" },
|
|
289
|
+
decorators: [
|
|
290
|
+
(story, context) => ({
|
|
291
|
+
components: { story, CanvasSwitcher },
|
|
292
|
+
setup() {
|
|
293
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
294
|
+
return { canvasName };
|
|
295
|
+
},
|
|
296
|
+
template: `
|
|
297
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
298
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
299
|
+
</div>
|
|
300
|
+
<div :class="canvasName" style="margin-inline: auto; padding: 2rem; outline: 1px dashed currentColor;">
|
|
301
|
+
<story />
|
|
302
|
+
</div>
|
|
303
|
+
`,
|
|
304
|
+
}),
|
|
305
|
+
],
|
|
306
|
+
render: (args) => ({
|
|
307
|
+
components: { AlertContent, InputButton },
|
|
308
|
+
setup() {
|
|
309
|
+
return { ...useStorySetup(args), stressAlerts };
|
|
310
|
+
},
|
|
311
|
+
template: `
|
|
312
|
+
<div :style="tokenStyles" style="display: grid; gap: 1.6rem;">
|
|
313
|
+
${toastNote}
|
|
314
|
+
<div v-for="alert in stressAlerts" :key="alert.key" :data-theme="alert.theme" :dir="alert.dir">
|
|
315
|
+
<AlertContent
|
|
316
|
+
:theme="alert.theme"
|
|
317
|
+
:custom-icon="alert.customIcon"
|
|
318
|
+
:show-icon="alert.showIcon ?? true"
|
|
319
|
+
:dismissible="alert.dismissible ?? false"
|
|
320
|
+
>
|
|
321
|
+
<template v-if="alert.title !== undefined" #title>{{ alert.title }}</template>
|
|
322
|
+
<template v-if="alert.content" #content>{{ alert.content }}</template>
|
|
323
|
+
<template v-if="alert.actions" #actions>
|
|
324
|
+
<InputButton
|
|
325
|
+
v-for="label in alert.actions"
|
|
326
|
+
:key="label"
|
|
327
|
+
type="button"
|
|
328
|
+
variant="primary"
|
|
329
|
+
:button-text="label"
|
|
330
|
+
/>
|
|
331
|
+
</template>
|
|
332
|
+
<template v-if="alert.dir === 'rtl'" #dismissLabel>إغلاق</template>
|
|
333
|
+
</AlertContent>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
`,
|
|
337
|
+
}),
|
|
338
|
+
parameters: {
|
|
339
|
+
layout: "fullscreen",
|
|
340
|
+
initialCanvas: "mobileCanvas",
|
|
341
|
+
docs: {
|
|
342
|
+
description: {
|
|
343
|
+
story:
|
|
344
|
+
"Hostile content: an unbroken German compound as the title, long German copy ending in a long URL, long German button labels, " +
|
|
345
|
+
"emoji at the start, HTML-like text (must render as text), right-to-left Arabic, a two-character title, an empty title slot, " +
|
|
346
|
+
"a broken customIcon name, and a one-character message with no icon. Check at every CanvasSwitcher width that nothing overflows " +
|
|
347
|
+
"or gets clipped by the alert's rounded edge, unbroken text wraps, the action buttons wrap their labels instead of spilling out, " +
|
|
348
|
+
"the icon and dismiss button keep their size, and the RTL alert puts the accent stripe on the right. " +
|
|
349
|
+
"The two line-clamp controls are CSS tokens (story only), not props: try `1` to see single-line ellipsis.",
|
|
350
|
+
},
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
@@ -156,6 +156,39 @@ describe("AlertContent", () => {
|
|
|
156
156
|
expect(wrapper.find("[data-test-id='alert-actions']").exists()).toBe(false);
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
// ─── styleClassPassthrough / hostile text (added 2026-10-08) ────────────────
|
|
160
|
+
|
|
161
|
+
it("applies styleClassPassthrough classes to the root", async () => {
|
|
162
|
+
const wrapper = await mountSuspended(AlertContent, {
|
|
163
|
+
props: { theme: "info", styleClassPassthrough: ["site-notice", "is-wide"] },
|
|
164
|
+
});
|
|
165
|
+
expect(wrapper.find(".alert-content").classes()).toEqual(expect.arrayContaining(["site-notice", "is-wide"]));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it("updates root classes when styleClassPassthrough changes", async () => {
|
|
169
|
+
const wrapper = await mountSuspended(AlertContent, {
|
|
170
|
+
props: { theme: "info", styleClassPassthrough: "first-class" },
|
|
171
|
+
});
|
|
172
|
+
await wrapper.setProps({ styleClassPassthrough: "second-class" });
|
|
173
|
+
const classes = wrapper.find(".alert-content").classes();
|
|
174
|
+
expect(classes).toContain("second-class");
|
|
175
|
+
expect(classes).not.toContain("first-class");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("renders HTML-like slot text as text, not markup", async () => {
|
|
179
|
+
const wrapper = await mountSuspended(
|
|
180
|
+
{
|
|
181
|
+
components: { AlertContent },
|
|
182
|
+
setup: () => ({ text: "<img src=x onerror=alert(1)>" }),
|
|
183
|
+
template: `<AlertContent theme="info"><template #content>{{ text }}</template></AlertContent>`,
|
|
184
|
+
},
|
|
185
|
+
{}
|
|
186
|
+
);
|
|
187
|
+
const content = wrapper.find("[data-test-id='alert-content']");
|
|
188
|
+
expect(content.find("img").exists()).toBe(false);
|
|
189
|
+
expect(content.text()).toBe("<img src=x onerror=alert(1)>");
|
|
190
|
+
});
|
|
191
|
+
|
|
159
192
|
it("uses component-prefixed classes for the title and text", async () => {
|
|
160
193
|
const wrapper = await mountSuspended(AlertContent, {
|
|
161
194
|
props: { theme: "info" },
|
|
@@ -6,13 +6,17 @@
|
|
|
6
6
|
|
|
7
7
|
| Token | Default | Controls |
|
|
8
8
|
|---|---|---|
|
|
9
|
-
| `--display-avatar-size` | the size
|
|
10
|
-
| `--display-avatar-font-size` | the size
|
|
9
|
+
| `--display-avatar-size` | the `size` step's value | Diameter, overriding the `size` prop's scale (one instance or a whole area) |
|
|
10
|
+
| `--display-avatar-font-size` | the `size` step's value | Initials/text size, overriding the scale |
|
|
11
11
|
| `--display-avatar-size-xs` / `-s` / `-md` / `-lg` / `-xl` | `2.4rem` / `3.2rem` / `4rem` / `4.8rem` / `5.6rem` | The diameter each `size` value maps to |
|
|
12
12
|
| `--display-avatar-font-size-xs` / `-s` / `-md` / `-lg` / `-xl` | `1.2rem` / `1.4rem` / `1.6rem` / `1.8rem` / `2rem` | The text size each `size` value maps to |
|
|
13
13
|
|
|
14
|
-
A custom `size` string (anything other than `xs`–`xl`) is
|
|
15
|
-
step, so it renders at `md` unless you set `--display-avatar-size` for it
|
|
14
|
+
A custom `size` string (anything other than `xs`–`xl`) is set as `data-size` but maps to no
|
|
15
|
+
scale step, so it renders at `md` unless you set `--display-avatar-size` for it (target it with
|
|
16
|
+
`.display-avatar[data-size="<your-size>"]`).
|
|
17
|
+
|
|
18
|
+
If you size an image avatar beyond the scale, raise the `image-size` prop (default `112`, the
|
|
19
|
+
pixel width and height requested from `@nuxt/image`) to match, or the image is upscaled and blurry.
|
|
16
20
|
|
|
17
21
|
### Appearance
|
|
18
22
|
|
|
@@ -30,18 +34,33 @@ step, so it renders at `md` unless you set `--display-avatar-size` for it.
|
|
|
30
34
|
> faint in light mode; it now uses the `--theme-*` surface and text tokens, so it follows light
|
|
31
35
|
> and dark mode.
|
|
32
36
|
|
|
33
|
-
Private tokens (not public API): `--
|
|
37
|
+
Private tokens (not public API): `--_avatar-size` and `--_avatar-font-size`, which each size step sets from its
|
|
34
38
|
scale token.
|
|
35
39
|
|
|
36
40
|
## State hooks
|
|
37
41
|
|
|
38
|
-
-
|
|
39
|
-
- Classes: `.display-avatar` (root), `.display-avatar-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
- `data-size` on the root: `xs`, `s`, `md`, `lg`, `xl` (or your custom `size` string).
|
|
43
|
+
- Classes: `.display-avatar` (root: size, font size, radius), `.display-avatar-surface` (fills the root,
|
|
44
|
+
carries the background and text colour, and holds all content), `.display-avatar-image` (the image), `.display-avatar-fallback`
|
|
45
|
+
(the initials or `text`; one line, clipped to the circle), `.display-avatar-icon` (opt-in, for
|
|
46
|
+
your `#icon` slot content).
|
|
47
|
+
- With `chip`, the root is `DisplayChip`, so it also carries `.display-chip` and its
|
|
48
|
+
`data-shape`/`data-status` attributes (status comes from the `status` prop). See `display-chip`
|
|
49
|
+
for its tokens.
|
|
50
|
+
|
|
51
|
+
> **Changed 2026-10-14**: the size was a bare class on the root (`.display-avatar.lg`), which a
|
|
52
|
+
> consumer's own `.lg`/`.xl` utility classes could match. Replace `.display-avatar.<size>` with
|
|
53
|
+
> `.display-avatar[data-size="<size>"]`. Overlong fallback text is now clipped inside the circle, the
|
|
54
|
+
> avatar no longer shrinks in a flex row, and a broken image falls back to the initials.
|
|
55
|
+
> With `chip`, the status dot is its configured size again (it had picked up the avatar's diameter
|
|
56
|
+
> through a shared private `--_size` name). Content now sits in a full-size
|
|
57
|
+
> `.display-avatar-surface`, which carries the background: the chip's cutout used to land on the
|
|
58
|
+
> small initials span (cutting into the letters) and never reached the background. If you set
|
|
59
|
+
> `background` directly on `.display-avatar`, use `--display-avatar-background` instead.
|
|
60
|
+
>
|
|
61
|
+
> **Changed 2026-10-08**: chip status is the `status` prop (forwarded to `DisplayChip` as
|
|
62
|
+
> `data-status`), not an `online`/`idle`/`dnd` class through `style-class-passthrough`.
|
|
63
|
+
>
|
|
45
64
|
> **Changed 2026-09-27**: `.avatar-image` → `.display-avatar-image` and `.avatar-icon` →
|
|
46
65
|
> `.display-avatar-icon`.
|
|
47
66
|
|
|
@@ -53,11 +72,10 @@ Set the tokens above on an element you own (a page or section class, or a class
|
|
|
53
72
|
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the
|
|
54
73
|
component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
55
74
|
|
|
56
|
-
**Caveat:** the size
|
|
75
|
+
**Caveat:** the size steps set `--_avatar-size`/`--_avatar-font-size` on the avatar itself, so changing the
|
|
57
76
|
scale (`--display-avatar-size-md` etc.) from an ancestor works, but a one-off diameter should use
|
|
58
77
|
`--display-avatar-size`, which beats the scale wherever it's set.
|
|
59
78
|
|
|
60
79
|
## Class passthrough
|
|
61
80
|
|
|
62
|
-
`style-class-passthrough` adds classes to the root, including when the root is `DisplayChip`.
|
|
63
|
-
how chip status classes like `online` are applied.
|
|
81
|
+
`style-class-passthrough` adds classes to the root, including when the root is `DisplayChip`.
|