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
|
@@ -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" },
|
|
@@ -38,9 +38,12 @@ scale token.
|
|
|
38
38
|
- Size class on the root: `.xs`, `.s`, `.md`, `.lg`, `.xl` (or your custom `size` string).
|
|
39
39
|
- Classes: `.display-avatar` (root), `.display-avatar-image` (the image), `.display-avatar-icon`
|
|
40
40
|
(opt-in, for your `#icon` slot content).
|
|
41
|
-
- With `chip`, the root is `DisplayChip`, so it also carries `.display-chip` and its
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
- With `chip`, the root is `DisplayChip`, so it also carries `.display-chip` and its
|
|
42
|
+
`data-shape`/`data-status` attributes (status comes from the `status` prop). See `display-chip`
|
|
43
|
+
for its tokens.
|
|
44
|
+
|
|
45
|
+
> **Changed 2026-10-08**: chip status is the `status` prop (forwarded to `DisplayChip` as
|
|
46
|
+
> `data-status`), not an `online`/`idle`/`dnd` class through `style-class-passthrough`.
|
|
44
47
|
|
|
45
48
|
> **Changed 2026-09-27**: `.avatar-image` → `.display-avatar-image` and `.avatar-icon` →
|
|
46
49
|
> `.display-avatar-icon`.
|
|
@@ -59,5 +62,4 @@ scale (`--display-avatar-size-md` etc.) from an ancestor works, but a one-off di
|
|
|
59
62
|
|
|
60
63
|
## Class passthrough
|
|
61
64
|
|
|
62
|
-
`style-class-passthrough` adds classes to the root, including when the root is `DisplayChip`.
|
|
63
|
-
how chip status classes like `online` are applied.
|
|
65
|
+
`style-class-passthrough` adds classes to the root, including when the root is `DisplayChip`.
|
|
@@ -1,7 +1,7 @@
|
|
|
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
6
|
:class="[size, elementClasses]"
|
|
7
7
|
>
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
<script setup lang="ts">
|
|
20
20
|
import DisplayChip from "../display-chip/DisplayChip.vue";
|
|
21
|
-
import type { DisplayChipConfig } from "~/types/components";
|
|
21
|
+
import type { DisplayChipConfig, DisplayChipStatus } from "~/types/components";
|
|
22
22
|
|
|
23
23
|
interface Props {
|
|
24
24
|
as?: string | object;
|
|
@@ -27,6 +27,8 @@ interface Props {
|
|
|
27
27
|
text?: string;
|
|
28
28
|
size?: "xs" | "s" | "md" | "lg" | "xl" | string;
|
|
29
29
|
chip?: boolean | DisplayChipConfig;
|
|
30
|
+
status?: DisplayChipStatus;
|
|
31
|
+
statusLabel?: string;
|
|
30
32
|
styleClassPassthrough?: string | string[];
|
|
31
33
|
}
|
|
32
34
|
|
|
@@ -37,6 +39,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
37
39
|
text: undefined,
|
|
38
40
|
size: "md",
|
|
39
41
|
chip: undefined,
|
|
42
|
+
status: undefined,
|
|
43
|
+
statusLabel: undefined,
|
|
40
44
|
styleClassPassthrough: () => [],
|
|
41
45
|
});
|
|
42
46
|
|
|
@@ -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,7 +92,9 @@ 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
|
|
|
@@ -102,6 +119,8 @@ const Template: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
102
119
|
:src="args.src"
|
|
103
120
|
:alt="args.alt"
|
|
104
121
|
:chip="chip"
|
|
122
|
+
:status="args.status"
|
|
123
|
+
:status-label="args.statusLabel"
|
|
105
124
|
:style-class-passthrough="args.styleClassPassthrough"
|
|
106
125
|
/>
|
|
107
126
|
</div>
|
|
@@ -117,8 +136,8 @@ export const Initials: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
117
136
|
},
|
|
118
137
|
template: `
|
|
119
138
|
<div style="display: flex; align-items: center; justify-content: center; gap: 1.6rem; height: 100vh;">
|
|
120
|
-
<StorybookComponent :size="args.size" :alt="args.alt" :chip="chip" :style-class-passthrough="args.styleClassPassthrough" />
|
|
121
|
-
<StorybookComponent :size="args.size" text="?" :chip="chip" :style-class-passthrough="args.styleClassPassthrough" />
|
|
139
|
+
<StorybookComponent :size="args.size" :alt="args.alt" :chip="chip" :status="args.status" :status-label="args.statusLabel" :style-class-passthrough="args.styleClassPassthrough" />
|
|
140
|
+
<StorybookComponent :size="args.size" text="?" :chip="chip" :status="args.status" :status-label="args.statusLabel" :style-class-passthrough="args.styleClassPassthrough" />
|
|
122
141
|
</div>
|
|
123
142
|
`,
|
|
124
143
|
});
|
|
@@ -139,6 +158,8 @@ export const Sizes: StoryFn<typeof StorybookComponent> = (args) => ({
|
|
|
139
158
|
:src="args.src"
|
|
140
159
|
:alt="args.alt"
|
|
141
160
|
:chip="chip"
|
|
161
|
+
:status="args.status"
|
|
162
|
+
:status-label="args.statusLabel"
|
|
142
163
|
:style-class-passthrough="args.styleClassPassthrough"
|
|
143
164
|
/>
|
|
144
165
|
</div>
|
|
@@ -232,6 +232,25 @@ describe("DisplayAvatar", () => {
|
|
|
232
232
|
expect(wrapper.classes()).toEqual(expect.arrayContaining(["display-chip", "display-avatar", "lg", "online"]));
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
it("forwards status and statusLabel to the chip", async () => {
|
|
236
|
+
const wrapper = await mountSuspended(DisplayAvatar, {
|
|
237
|
+
props: { chip: true, alt: "Jane Smith", status: "dnd", statusLabel: "Do not disturb" },
|
|
238
|
+
});
|
|
239
|
+
expect(wrapper.attributes("data-status")).toBe("dnd");
|
|
240
|
+
expect(wrapper.findAll(".sr-only").map((el) => el.text())).toContain("Do not disturb");
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("leaves the chip offline when no status is given", async () => {
|
|
244
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { chip: true } });
|
|
245
|
+
expect(wrapper.attributes("data-status")).toBe("offline");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("does not put status attributes on a plain avatar", async () => {
|
|
249
|
+
const wrapper = await mountSuspended(DisplayAvatar, { props: { status: "online" } });
|
|
250
|
+
expect(wrapper.attributes("data-status")).toBeUndefined();
|
|
251
|
+
expect(wrapper.attributes("status")).toBeUndefined();
|
|
252
|
+
});
|
|
253
|
+
|
|
235
254
|
it("updates the chip config when the chip prop changes", async () => {
|
|
236
255
|
const wrapper = await mountSuspended(DisplayAvatar, { props: { chip: true } });
|
|
237
256
|
await wrapper.setProps({ chip: { size: "20px", maskWidth: "2px", offset: "0px", angle: "0deg" } });
|
package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
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>"`;
|
|
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 md" data-shape="circle" data-status="offline" style="--_chip-size: 12px; --_chip-mask-width: 4px; --_chip-offset: 0px; --_chip-angle: 90deg;"><span aria-hidden="true">JD</span><span class="sr-only">John Doe</span>
|
|
7
|
+
<!--v-if-->
|
|
7
8
|
<!--v-if-->
|
|
8
9
|
<!--v-if--></span>"
|
|
9
10
|
`;
|