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,7 +1,9 @@
|
|
|
1
|
-
import { ref } from "vue";
|
|
1
|
+
import { computed, ref } from "vue";
|
|
2
2
|
import type { Meta, StoryObj } from "@nuxtjs/storybook";
|
|
3
3
|
import SelectMenu from "../SelectMenu.vue";
|
|
4
|
+
import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
|
|
4
5
|
import type { SelectMenuOption } from "~/types/components/select-menu";
|
|
6
|
+
import type { MediaCanvas } from "~/types/components";
|
|
5
7
|
|
|
6
8
|
// ─── Meta ─────────────────────────────────────────────────────────────────────
|
|
7
9
|
|
|
@@ -10,13 +12,18 @@ interface StoryArgs {
|
|
|
10
12
|
label?: string;
|
|
11
13
|
placeholder?: string;
|
|
12
14
|
showIcon?: boolean;
|
|
15
|
+
triggerIcon?: string;
|
|
16
|
+
indicator?: "none" | "count" | "dot";
|
|
17
|
+
selectedCountLabel?: string;
|
|
13
18
|
showLabel?: boolean;
|
|
14
19
|
showChevron?: boolean;
|
|
15
20
|
multiple?: boolean;
|
|
16
21
|
showSelectionInTrigger?: boolean;
|
|
22
|
+
reserveLabelWidth?: boolean;
|
|
17
23
|
inputVariant?: "normal" | "underlined";
|
|
18
24
|
modelValue?: string | number | (string | number)[];
|
|
19
25
|
styleClassPassthrough?: string | string[];
|
|
26
|
+
itemLabelLineClamp?: string;
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
const meta: Meta<StoryArgs> = {
|
|
@@ -35,9 +42,25 @@ const meta: Meta<StoryArgs> = {
|
|
|
35
42
|
},
|
|
36
43
|
showIcon: {
|
|
37
44
|
control: { type: "boolean" },
|
|
38
|
-
description: "Show the selected option's icon
|
|
45
|
+
description: "Show the leading icon in the trigger: the selected option's icon (single-select), otherwise `triggerIcon`.",
|
|
39
46
|
table: { category: "Trigger content" },
|
|
40
47
|
},
|
|
48
|
+
triggerIcon: {
|
|
49
|
+
control: { type: "text" },
|
|
50
|
+
description: "Fixed leading icon, e.g. a filter category icon. In single-select a selected option's own icon replaces it.",
|
|
51
|
+
table: { category: "Trigger content" },
|
|
52
|
+
},
|
|
53
|
+
indicator: {
|
|
54
|
+
control: { type: "select" },
|
|
55
|
+
options: ["none", "count", "dot"],
|
|
56
|
+
description: "Selection indicator after the trigger text, shown only while something is selected: a count badge or a status dot.",
|
|
57
|
+
table: { category: "Trigger content" },
|
|
58
|
+
},
|
|
59
|
+
selectedCountLabel: {
|
|
60
|
+
control: { type: "text" },
|
|
61
|
+
description: "Appended to the trigger's accessible name while the indicator shows. `{count}` is replaced with the number selected.",
|
|
62
|
+
table: { category: "Content" },
|
|
63
|
+
},
|
|
41
64
|
showLabel: {
|
|
42
65
|
control: { type: "boolean" },
|
|
43
66
|
description: "Show the selected option's label text in the trigger. Turn off for an icon-only compact trigger.",
|
|
@@ -58,6 +81,11 @@ const meta: Meta<StoryArgs> = {
|
|
|
58
81
|
description: "In multiple mode, update the trigger text to a comma-separated list of the currently checked options instead of leaving it fixed on placeholder/label. No effect outside multiple mode.",
|
|
59
82
|
table: { category: "Content" },
|
|
60
83
|
},
|
|
84
|
+
reserveLabelWidth: {
|
|
85
|
+
control: { type: "boolean" },
|
|
86
|
+
description: "Single-select only. Size the trigger text to the longest option label (or placeholder), so the trigger doesn't change width as the selection changes.",
|
|
87
|
+
table: { category: "Trigger content" },
|
|
88
|
+
},
|
|
61
89
|
inputVariant: {
|
|
62
90
|
control: { type: "select" },
|
|
63
91
|
options: ["normal", "underlined"],
|
|
@@ -67,10 +95,18 @@ const meta: Meta<StoryArgs> = {
|
|
|
67
95
|
styleClassPassthrough: {
|
|
68
96
|
table: { disable: true },
|
|
69
97
|
},
|
|
98
|
+
itemLabelLineClamp: {
|
|
99
|
+
control: { type: "select" },
|
|
100
|
+
options: ["none", "1", "2", "3"],
|
|
101
|
+
description:
|
|
102
|
+
"**Story control, not a prop.** Sets `--select-menu-item-label-line-clamp` on a wrapper (StressTest only). In your CSS: `.booking-filters { --select-menu-item-label-line-clamp: 2; }`",
|
|
103
|
+
table: { category: "CSS tokens (story only, set in your CSS)" },
|
|
104
|
+
},
|
|
70
105
|
},
|
|
71
106
|
args: {
|
|
72
107
|
label: "Language",
|
|
73
108
|
showIcon: true,
|
|
109
|
+
indicator: "none",
|
|
74
110
|
showLabel: true,
|
|
75
111
|
showChevron: true,
|
|
76
112
|
inputVariant: "normal",
|
|
@@ -328,6 +364,75 @@ export const MultiSelectShowingSelection: Story = {
|
|
|
328
364
|
}),
|
|
329
365
|
};
|
|
330
366
|
|
|
367
|
+
/**
|
|
368
|
+
* Filter with a count — a fixed category icon (`triggerIcon`) plus a count
|
|
369
|
+
* badge (`indicator="count"`) showing how many options are checked. The
|
|
370
|
+
* badge only appears once something is selected.
|
|
371
|
+
*/
|
|
372
|
+
export const FilterWithCount: Story = {
|
|
373
|
+
name: "Filter — Icon + Count",
|
|
374
|
+
args: {
|
|
375
|
+
label: "Category",
|
|
376
|
+
multiple: true,
|
|
377
|
+
triggerIcon: "lucide:list",
|
|
378
|
+
indicator: "count",
|
|
379
|
+
},
|
|
380
|
+
render: (args) => ({
|
|
381
|
+
components: { SelectMenu },
|
|
382
|
+
setup() {
|
|
383
|
+
const value = ref<string[]>(["trim", "layers", "restyle"]);
|
|
384
|
+
return { args, value, treatmentOptions };
|
|
385
|
+
},
|
|
386
|
+
template: `
|
|
387
|
+
<div style="padding: 4rem 8rem;">
|
|
388
|
+
<SelectMenu v-bind="args" v-model="value" :options="treatmentOptions" />
|
|
389
|
+
<p style="margin: 1.6rem 0 0; font-size: 1.3rem; opacity: 0.6;">
|
|
390
|
+
modelValue: <strong>{{ value.length ? value.join(', ') : '[]' }}</strong>
|
|
391
|
+
</p>
|
|
392
|
+
</div>
|
|
393
|
+
`,
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Filter with a status dot — single-select with `indicator="dot"`, e.g. a
|
|
399
|
+
* status filter. The dot signals the filter is active and disappears when the
|
|
400
|
+
* value is cleared. Each option's `dotColor` colours the trigger dot while
|
|
401
|
+
* it's selected and shows beside it in the list. `reserveLabelWidth` keeps
|
|
402
|
+
* the trigger at the width of the longest status, so it doesn't resize between
|
|
403
|
+
* "Active" and "Archived" (toggle it off in Controls to compare).
|
|
404
|
+
*/
|
|
405
|
+
export const FilterWithDot: Story = {
|
|
406
|
+
name: "Filter — Icon + Status Dot",
|
|
407
|
+
args: {
|
|
408
|
+
label: "Status",
|
|
409
|
+
triggerIcon: "lucide:circle-dot",
|
|
410
|
+
indicator: "dot",
|
|
411
|
+
reserveLabelWidth: true,
|
|
412
|
+
},
|
|
413
|
+
render: (args) => ({
|
|
414
|
+
components: { SelectMenu },
|
|
415
|
+
setup() {
|
|
416
|
+
const value = ref<string | undefined>("active");
|
|
417
|
+
const statusOptions: SelectMenuOption[] = [
|
|
418
|
+
{ value: "active", label: "Active", dotColor: "var(--status-success)" },
|
|
419
|
+
{ value: "pending", label: "Pending", dotColor: "var(--status-warning)" },
|
|
420
|
+
{ value: "archived", label: "Archived", dotColor: "var(--status-neutral)" },
|
|
421
|
+
];
|
|
422
|
+
return { args, value, statusOptions };
|
|
423
|
+
},
|
|
424
|
+
template: `
|
|
425
|
+
<div style="padding: 4rem 8rem;">
|
|
426
|
+
<SelectMenu v-bind="args" v-model="value" :options="statusOptions" />
|
|
427
|
+
<p style="margin: 1.6rem 0 0; font-size: 1.3rem; opacity: 0.6;">
|
|
428
|
+
modelValue: <strong>{{ value === undefined ? 'undefined' : value }}</strong>
|
|
429
|
+
<button type="button" style="margin-inline-start: 1.2rem;" @click="value = undefined">Clear</button>
|
|
430
|
+
</p>
|
|
431
|
+
</div>
|
|
432
|
+
`,
|
|
433
|
+
}),
|
|
434
|
+
};
|
|
435
|
+
|
|
331
436
|
/**
|
|
332
437
|
* Custom tokens — a per-instance styleClassPassthrough overriding the public
|
|
333
438
|
* --select-menu-* tokens to demonstrate full visual customisation.
|
|
@@ -392,3 +497,128 @@ export const Underlined: Story = {
|
|
|
392
497
|
`,
|
|
393
498
|
}),
|
|
394
499
|
};
|
|
500
|
+
|
|
501
|
+
// ─── Stress test ──────────────────────────────────────────────────────────────
|
|
502
|
+
|
|
503
|
+
const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
|
|
504
|
+
|
|
505
|
+
const stressOptions: SelectMenuOption[] = [
|
|
506
|
+
{
|
|
507
|
+
value: "long",
|
|
508
|
+
label:
|
|
509
|
+
"Full head balayage with toner, Olaplex bond treatment, deep conditioning mask and a blow-dry finish (allow 4 hours)",
|
|
510
|
+
icon: "lucide:sparkles",
|
|
511
|
+
},
|
|
512
|
+
{ value: "unbroken", label: longUnbroken },
|
|
513
|
+
{ value: "url", label: "https://example.com/" + "a-very-long-path-segment-without-spaces".repeat(3) },
|
|
514
|
+
{ value: "emoji", label: "🎉 Emoji first option", dotColor: "var(--status-success)" },
|
|
515
|
+
{ value: "html", label: "<b>Not bold</b> <script>alert('x')</script>" },
|
|
516
|
+
{ value: "rtl", label: "قص الشعر وتصفيفه" },
|
|
517
|
+
{ value: "single", label: "A" },
|
|
518
|
+
{ value: "empty", label: "" },
|
|
519
|
+
{ value: "broken-icon", label: "Broken icon name", icon: "lucide:does-not-exist" },
|
|
520
|
+
{ value: "bad-dot", label: "Invalid dot colour", dotColor: "not-a-colour" },
|
|
521
|
+
...Array.from({ length: 40 }, (_, index) => ({ value: `extra-${index + 1}`, label: `Option ${index + 1}` })),
|
|
522
|
+
];
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Stress test — hostile option data at every canvas width.
|
|
526
|
+
*/
|
|
527
|
+
export const StressTest: Story = {
|
|
528
|
+
name: "Stress Test (Worst-Case Data)",
|
|
529
|
+
args: {
|
|
530
|
+
label: "Behandlung auswählen für Ihren nächsten Termin im Salon (sehr lange Beschriftung)",
|
|
531
|
+
placeholder: "Bitte wählen Sie eine oder mehrere Behandlungen aus der Liste",
|
|
532
|
+
indicator: "count",
|
|
533
|
+
selectedCountLabel: "{count} ausgewählt",
|
|
534
|
+
itemLabelLineClamp: "none",
|
|
535
|
+
},
|
|
536
|
+
render: (args) => ({
|
|
537
|
+
components: { SelectMenu },
|
|
538
|
+
setup() {
|
|
539
|
+
const componentArgs = computed(() => {
|
|
540
|
+
const { itemLabelLineClamp: _itemLabelLineClamp, ...rest } = args;
|
|
541
|
+
return rest;
|
|
542
|
+
});
|
|
543
|
+
const wrapperStyle = computed(() => ({
|
|
544
|
+
"--select-menu-item-label-line-clamp": args.itemLabelLineClamp,
|
|
545
|
+
}));
|
|
546
|
+
const single = ref<string | undefined>("long");
|
|
547
|
+
const reserved = ref<string | undefined>("unbroken");
|
|
548
|
+
const many = ref<string[]>(stressOptions.map((option) => option.value as string));
|
|
549
|
+
const none = ref<string | undefined>(undefined);
|
|
550
|
+
return { componentArgs, wrapperStyle, single, reserved, many, none, stressOptions };
|
|
551
|
+
},
|
|
552
|
+
template: `
|
|
553
|
+
<div :style="wrapperStyle" style="display: grid; gap: 2.4rem; padding: 2.4rem 1.6rem;">
|
|
554
|
+
<section>
|
|
555
|
+
<h3 style="margin: 0 0 0.8rem;">Single-select, longest option selected</h3>
|
|
556
|
+
<SelectMenu v-bind="componentArgs" v-model="single" :options="stressOptions" :multiple="false" />
|
|
557
|
+
</section>
|
|
558
|
+
<section>
|
|
559
|
+
<h3 style="margin: 0 0 0.8rem;">Reserved label width, unbroken word selected, status dot</h3>
|
|
560
|
+
<SelectMenu
|
|
561
|
+
v-bind="componentArgs"
|
|
562
|
+
v-model="reserved"
|
|
563
|
+
:options="stressOptions"
|
|
564
|
+
:multiple="false"
|
|
565
|
+
:reserve-label-width="true"
|
|
566
|
+
indicator="dot"
|
|
567
|
+
/>
|
|
568
|
+
</section>
|
|
569
|
+
<section>
|
|
570
|
+
<h3 style="margin: 0 0 0.8rem;">Multi-select, all 50 checked, selection shown in trigger</h3>
|
|
571
|
+
<SelectMenu
|
|
572
|
+
v-bind="componentArgs"
|
|
573
|
+
v-model="many"
|
|
574
|
+
:options="stressOptions"
|
|
575
|
+
:multiple="true"
|
|
576
|
+
:show-selection-in-trigger="true"
|
|
577
|
+
trigger-icon="lucide:list-filter"
|
|
578
|
+
/>
|
|
579
|
+
</section>
|
|
580
|
+
<section style="display: flex; justify-content: flex-end;">
|
|
581
|
+
<div>
|
|
582
|
+
<h3 style="margin: 0 0 0.8rem;">Right-aligned trigger (menu should stay on screen)</h3>
|
|
583
|
+
<SelectMenu v-bind="componentArgs" v-model="none" :options="stressOptions" :multiple="false" :show-label="false" trigger-icon="lucide:list-filter" />
|
|
584
|
+
</div>
|
|
585
|
+
</section>
|
|
586
|
+
<section>
|
|
587
|
+
<h3 style="margin: 0 0 0.8rem;">No options</h3>
|
|
588
|
+
<SelectMenu v-bind="componentArgs" v-model="none" :options="[]" :multiple="false" />
|
|
589
|
+
</section>
|
|
590
|
+
</div>
|
|
591
|
+
`,
|
|
592
|
+
}),
|
|
593
|
+
decorators: [
|
|
594
|
+
(story, context) => ({
|
|
595
|
+
components: { story, CanvasSwitcher },
|
|
596
|
+
setup() {
|
|
597
|
+
const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "mobileCanvas");
|
|
598
|
+
return { canvasName };
|
|
599
|
+
},
|
|
600
|
+
template: `
|
|
601
|
+
<div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
|
|
602
|
+
<CanvasSwitcher v-model:canvas-name="canvasName" />
|
|
603
|
+
</div>
|
|
604
|
+
<div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
|
|
605
|
+
<story />
|
|
606
|
+
</div>
|
|
607
|
+
`,
|
|
608
|
+
}),
|
|
609
|
+
],
|
|
610
|
+
parameters: {
|
|
611
|
+
initialCanvas: "mobileCanvas",
|
|
612
|
+
docs: {
|
|
613
|
+
description: {
|
|
614
|
+
story:
|
|
615
|
+
"Hostile option data: a very long label, an unbroken word, a long URL, emoji first, HTML-like text (must render as text), " +
|
|
616
|
+
"RTL, a single character, an empty label, a broken icon name, an invalid dot colour, and 50 options in total. " +
|
|
617
|
+
"Check at every canvas width that each trigger stays inside the canvas with its text truncated by an ellipsis, the count badge " +
|
|
618
|
+
"and chevron stay visible, the menu never runs off either side of the screen (the right-aligned trigger flips its menu), " +
|
|
619
|
+
"long options wrap inside the menu, and the list scrolls. `itemLabelLineClamp` is a story-only control for the " +
|
|
620
|
+
"`--select-menu-item-label-line-clamp` CSS token: try `1` for single-line ellipsis. The empty menu shows just its border.",
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
};
|
|
@@ -462,6 +462,157 @@ describe("SelectMenu", () => {
|
|
|
462
462
|
});
|
|
463
463
|
});
|
|
464
464
|
|
|
465
|
+
// -------------------------
|
|
466
|
+
// Trigger icon + indicator
|
|
467
|
+
// -------------------------
|
|
468
|
+
describe("Trigger icon and indicator", () => {
|
|
469
|
+
const plainOptions: SelectMenuOption[] = [
|
|
470
|
+
{ value: "a", label: "Alpha" },
|
|
471
|
+
{ value: "b", label: "Beta" },
|
|
472
|
+
{ value: "c", label: "Gamma" },
|
|
473
|
+
];
|
|
474
|
+
|
|
475
|
+
it("renders triggerIcon in multiple mode", async () => {
|
|
476
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], triggerIcon: "lucide:list" });
|
|
477
|
+
expect(wrapper.find(".select-menu-trigger-icon").exists()).toBe(true);
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
it("renders triggerIcon in single mode when the selected option has no icon", async () => {
|
|
481
|
+
wrapper = await createWrapper({ options: plainOptions, modelValue: "a", triggerIcon: "lucide:list" });
|
|
482
|
+
expect(wrapper.find(".select-menu-trigger-icon").exists()).toBe(true);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
it("hides triggerIcon when showIcon is false", async () => {
|
|
486
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], triggerIcon: "lucide:list", showIcon: false });
|
|
487
|
+
expect(wrapper.find(".select-menu-trigger-icon").exists()).toBe(false);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it("renders no indicator by default", async () => {
|
|
491
|
+
wrapper = await createWrapper({ multiple: true, modelValue: ["en", "fr"] });
|
|
492
|
+
expect(wrapper.find(".select-menu-trigger-count").exists()).toBe(false);
|
|
493
|
+
expect(wrapper.find(".select-menu-trigger-dot").exists()).toBe(false);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
it("shows the number selected in the count badge", async () => {
|
|
497
|
+
wrapper = await createWrapper({ multiple: true, modelValue: ["en", "fr"], indicator: "count" });
|
|
498
|
+
expect(wrapper.find(".select-menu-trigger-count").text()).toBe("2");
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it("keeps an empty, hidden count badge in the layout when nothing is selected", async () => {
|
|
502
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], indicator: "count" });
|
|
503
|
+
const badge = wrapper.find(".select-menu-trigger-count");
|
|
504
|
+
expect(badge.exists()).toBe(true);
|
|
505
|
+
expect(badge.text()).toBe("");
|
|
506
|
+
expect(badge.classes()).toContain("select-menu-trigger-indicator-empty");
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it("updates the count badge as options are toggled", async () => {
|
|
510
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], indicator: "count" });
|
|
511
|
+
await wrapper.setProps({ modelValue: ["en", "fr", "de"] });
|
|
512
|
+
const badge = wrapper.find(".select-menu-trigger-count");
|
|
513
|
+
expect(badge.text()).toBe("3");
|
|
514
|
+
expect(badge.classes()).not.toContain("select-menu-trigger-indicator-empty");
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it("hides the dot, keeping its space, when nothing is selected", async () => {
|
|
518
|
+
wrapper = await createWrapper({ modelValue: "en", indicator: "dot" });
|
|
519
|
+
const dot = () => wrapper.find(".select-menu-trigger-dot");
|
|
520
|
+
expect(dot().classes()).not.toContain("select-menu-trigger-indicator-empty");
|
|
521
|
+
await wrapper.setProps({ modelValue: undefined });
|
|
522
|
+
expect(dot().exists()).toBe(true);
|
|
523
|
+
expect(dot().classes()).toContain("select-menu-trigger-indicator-empty");
|
|
524
|
+
});
|
|
525
|
+
|
|
526
|
+
it("colours the trigger dot with the selected option's dotColor", async () => {
|
|
527
|
+
const statusOptions: SelectMenuOption[] = [
|
|
528
|
+
{ value: "active", label: "Active", dotColor: "green" },
|
|
529
|
+
{ value: "pending", label: "Pending", dotColor: "orange" },
|
|
530
|
+
];
|
|
531
|
+
wrapper = await createWrapper({ options: statusOptions, modelValue: "active", indicator: "dot" });
|
|
532
|
+
const dot = () => wrapper.find(".select-menu-trigger-dot").element as HTMLElement;
|
|
533
|
+
expect(dot().style.getPropertyValue("--_dot-color")).toBe("green");
|
|
534
|
+
await wrapper.setProps({ modelValue: "pending" });
|
|
535
|
+
expect(dot().style.getPropertyValue("--_dot-color")).toBe("orange");
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it("leaves the trigger dot on the token colour when the option has no dotColor", async () => {
|
|
539
|
+
wrapper = await createWrapper({ options: plainOptions, modelValue: "a", indicator: "dot" });
|
|
540
|
+
const dot = wrapper.find(".select-menu-trigger-dot").element as HTMLElement;
|
|
541
|
+
expect(dot.style.getPropertyValue("--_dot-color")).toBe("");
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it("renders a list dot for options with dotColor and no icon", async () => {
|
|
545
|
+
wrapper = await createWrapper({
|
|
546
|
+
options: [
|
|
547
|
+
{ value: "a", label: "Alpha", dotColor: "green" },
|
|
548
|
+
{ value: "b", label: "Beta" },
|
|
549
|
+
{ value: "c", label: "Gamma", icon: "lucide:star", dotColor: "red" },
|
|
550
|
+
],
|
|
551
|
+
});
|
|
552
|
+
const rows = wrapper.findAll(".select-menu-list-item");
|
|
553
|
+
const rowDot = rows[0]!.find(".select-menu-item-dot");
|
|
554
|
+
expect((rowDot.element as HTMLElement).style.getPropertyValue("--_dot-color")).toBe("green");
|
|
555
|
+
expect(rows[1]!.find(".select-menu-item-dot").exists()).toBe(false);
|
|
556
|
+
expect(rows[2]!.find(".select-menu-item-dot").exists()).toBe(false);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it("hides the indicator from assistive tech", async () => {
|
|
560
|
+
wrapper = await createWrapper({ multiple: true, modelValue: ["en"], indicator: "count" });
|
|
561
|
+
expect(wrapper.find(".select-menu-trigger-count").attributes("aria-hidden")).toBe("true");
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
it("appends the selected count to the trigger's accessible name while the indicator shows", async () => {
|
|
565
|
+
wrapper = await createWrapper({ multiple: true, modelValue: ["en", "fr"], indicator: "count" });
|
|
566
|
+
expect(wrapper.find(".select-menu-trigger").attributes("aria-label")).toBe("Language, 2 selected");
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("uses a custom selectedCountLabel", async () => {
|
|
570
|
+
wrapper = await createWrapper({
|
|
571
|
+
multiple: true,
|
|
572
|
+
modelValue: ["en"],
|
|
573
|
+
indicator: "count",
|
|
574
|
+
selectedCountLabel: "{count} sélectionné",
|
|
575
|
+
});
|
|
576
|
+
expect(wrapper.find(".select-menu-trigger").attributes("aria-label")).toBe("Language, 1 sélectionné");
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
it("keeps the plain label as the accessible name when nothing is selected", async () => {
|
|
580
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], indicator: "count" });
|
|
581
|
+
expect(wrapper.find(".select-menu-trigger").attributes("aria-label")).toBe("Language");
|
|
582
|
+
});
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
// -------------------------
|
|
586
|
+
// reserveLabelWidth
|
|
587
|
+
// -------------------------
|
|
588
|
+
describe("reserveLabelWidth", () => {
|
|
589
|
+
it("renders plain label text by default", async () => {
|
|
590
|
+
wrapper = await createWrapper({ modelValue: "en" });
|
|
591
|
+
expect(wrapper.find(".select-menu-trigger-label-sizer").exists()).toBe(false);
|
|
592
|
+
expect(wrapper.find(".select-menu-trigger-label").text()).toBe("English");
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
it("renders a hidden sizer for the placeholder and every option label", async () => {
|
|
596
|
+
wrapper = await createWrapper({ modelValue: "en", placeholder: "Pick one", reserveLabelWidth: true });
|
|
597
|
+
const sizers = wrapper.findAll(".select-menu-trigger-label-sizer");
|
|
598
|
+
expect(sizers.map((sizer) => sizer.text())).toEqual(["Pick one", "English", "Français", "Deutsch"]);
|
|
599
|
+
expect(sizers.every((sizer) => sizer.attributes("aria-hidden") === "true")).toBe(true);
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
it("shows the selected label in the visible text span", async () => {
|
|
603
|
+
wrapper = await createWrapper({ modelValue: "fr", reserveLabelWidth: true });
|
|
604
|
+
expect(wrapper.find(".select-menu-trigger-label-text").text()).toBe("Français");
|
|
605
|
+
await wrapper.setProps({ modelValue: "de" });
|
|
606
|
+
expect(wrapper.find(".select-menu-trigger-label-text").text()).toBe("Deutsch");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
it("has no effect in multiple mode", async () => {
|
|
610
|
+
wrapper = await createWrapper({ multiple: true, modelValue: [], reserveLabelWidth: true });
|
|
611
|
+
expect(wrapper.find(".select-menu-trigger-label").classes()).not.toContain("select-menu-trigger-label-reserved");
|
|
612
|
+
expect(wrapper.find(".select-menu-trigger-label-sizer").exists()).toBe(false);
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
|
|
465
616
|
// -------------------------
|
|
466
617
|
// styleClassPassthrough
|
|
467
618
|
// -------------------------
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
exports[`SelectMenu > Snapshots > default 1`] = `
|
|
4
4
|
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false">
|
|
5
|
-
<!--v-if--><span class="select-menu-trigger-label">Language</span
|
|
5
|
+
<!--v-if--><span class="select-menu-trigger-label">Language</span>
|
|
6
|
+
<!--v-if--><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span>
|
|
6
7
|
</button>
|
|
7
8
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
8
9
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
@@ -18,6 +19,7 @@ exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
|
18
19
|
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:gb-4x3 select-menu-trigger-icon" aria-hidden="true"></span>
|
|
19
20
|
<!--v-if-->
|
|
20
21
|
<!--v-if-->
|
|
22
|
+
<!--v-if-->
|
|
21
23
|
</button>
|
|
22
24
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
23
25
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
@@ -30,7 +32,9 @@ exports[`SelectMenu > Snapshots > icon-only trigger 1`] = `
|
|
|
30
32
|
`;
|
|
31
33
|
|
|
32
34
|
exports[`SelectMenu > Snapshots > with a selected value 1`] = `
|
|
33
|
-
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span
|
|
35
|
+
"<div class="select-menu normal" style="--_anchor-name: --select-menu-anchor-v-0-0;"><button popovertarget="select-menu-v-0-0" popovertargetaction="toggle" type="button" class="select-menu-trigger" aria-label="Language" aria-haspopup="listbox" aria-expanded="false"><span class="iconify i-flag:fr-4x3 select-menu-trigger-icon" aria-hidden="true"></span><span class="select-menu-trigger-label">Français</span>
|
|
36
|
+
<!--v-if--><span class="iconify i-lucide:chevron-down select-menu-trigger-chevron" aria-hidden="true"></span>
|
|
37
|
+
</button>
|
|
34
38
|
<div id="select-menu-v-0-0" popover="" class="select-menu-popover">
|
|
35
39
|
<ul class="select-menu-list" role="listbox" aria-label="Language">
|
|
36
40
|
<li class="select-menu-list-item" role="option" tabindex="-1" aria-selected="false" data-label="English"><span class="select-menu-item-check" aria-hidden="true"><!--v-if--></span><span class="iconify i-flag:gb-4x3 select-menu-item-icon" aria-hidden="true"></span><span class="select-menu-item-label">English</span></li>
|
|
@@ -1,41 +1,105 @@
|
|
|
1
1
|
# DisplayToast / DisplayToastProvider — Consumer Styling Guide
|
|
2
2
|
|
|
3
|
+
Both render the toast body with `AlertContent` (or `AlertMaskedContent` when `masked`), so the
|
|
4
|
+
card's colours, radius and accent bar come from `--alert-content-*` tokens: see
|
|
5
|
+
`02.molecules/alert-content/CONSUMER-STYLING.md`. The tokens below cover the toast's own shell.
|
|
6
|
+
|
|
7
|
+
## Public token API
|
|
8
|
+
|
|
9
|
+
### DisplayToast (standalone)
|
|
10
|
+
|
|
11
|
+
| Token | Default | Description |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| `--display-toast-z-index` | `999999` | Stacking order (see Stacking order). |
|
|
14
|
+
| `--display-toast-max-width` | `min(48rem, 100% - 2 * gutter)` | Maximum toast width. Ignored when `fullWidth` is set. |
|
|
15
|
+
| `--display-toast-progress-colour` | `var(--theme-accent)` | Auto-dismiss progress bar colour. |
|
|
16
|
+
| `--display-toast-focus-ring-colour` | `var(--theme-ring)` | Keyboard focus outline colour. |
|
|
17
|
+
|
|
18
|
+
### DisplayToastProvider (queue)
|
|
19
|
+
|
|
20
|
+
| Token | Default | Description |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `--display-toast-provider-z-index` | `999999` | Stacking order (see Stacking order). |
|
|
23
|
+
| `--display-toast-provider-max-width` | `min(48rem, 100% - 2 * gutter)` | Maximum width of the toast stack. Ignored when `fullWidth` is set. |
|
|
24
|
+
| `--display-toast-provider-progress-colour` | `var(--theme-accent)` | Auto-dismiss progress bar colour. |
|
|
25
|
+
| `--display-toast-provider-focus-ring-colour` | `var(--theme-ring)` | Keyboard focus outline colour. |
|
|
26
|
+
|
|
27
|
+
The gutter is `12px` below `600px` viewport width, `24px` above.
|
|
28
|
+
|
|
29
|
+
> Changed 2026-10-08: the max-width tokens are new. The standalone centred toast used
|
|
30
|
+
> `width: max-content` with no cap, so long text made it wider than the screen; right/left-aligned
|
|
31
|
+
> toasts and the provider stack could stretch almost the full viewport on wide screens. Both now
|
|
32
|
+
> cap at `48rem`. Set the max-width token to `none` (with the gutter still applying) to undo that.
|
|
33
|
+
> The progress and focus-ring colour tokens are also new; they used to read `--theme-accent` and
|
|
34
|
+
> `--theme-ring` directly.
|
|
35
|
+
|
|
36
|
+
Private tokens, not public API: `--_toast-gutter` and `--_gutter` (the responsive gutter), and the
|
|
37
|
+
provider's `--_reveal` / `--_duration` (set inline per toast from its config).
|
|
38
|
+
|
|
39
|
+
## State hooks
|
|
40
|
+
|
|
41
|
+
| Hook | Where | Meaning |
|
|
42
|
+
|---|---|---|
|
|
43
|
+
| `[data-position="top\|bottom"]` | `.display-toast`, `.display-toast-provider` | Vertical position. |
|
|
44
|
+
| `[data-alignment="left\|center\|right\|full-width"]` | `.display-toast`, `.display-toast-provider` | Horizontal alignment; `full-width` when `fullWidth` is set. Below `600px` every toast is centred. |
|
|
45
|
+
| `[data-state="show\|hide"]` | `.display-toast` | Entering or leaving (standalone only; the provider uses `TransitionGroup`'s `v-enter-*`/`v-leave-*` classes). |
|
|
46
|
+
| `[data-paused]` | `.display-toast`, `.display-toast-provider-item` | Auto-dismiss is paused (hovered, or keyboard focus on or inside the toast). Freezes the progress bar. |
|
|
47
|
+
| `[data-theme]` | `.display-toast`, `.display-toast-provider-item` | The toast's semantic theme. |
|
|
48
|
+
|
|
49
|
+
Inner elements: `.display-toast-progress`, `.display-toast-provider-item`,
|
|
50
|
+
`.display-toast-provider-progress`.
|
|
51
|
+
|
|
52
|
+
> Changed 2026-10-08: position, alignment and state were bare classes (`.top`, `.left`, `.center`,
|
|
53
|
+
> `.full-width`, `.show`, `.hide`). The toast is teleported into `<body>`, so a consumer's own
|
|
54
|
+
> utility classes with those names (a `.hide { display: none }` especially) could match it. They're
|
|
55
|
+
> `data-*` attributes now; update any selector that used the old classes. The keyframes were
|
|
56
|
+
> renamed for the same reason (`show`, `progress`, `hideTop`, … are now `display-toast-*` and
|
|
57
|
+
> `display-toast-provider-*`).
|
|
58
|
+
|
|
3
59
|
## Stacking order
|
|
4
60
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
61
|
+
Both z-index tokens default to `999999`, matching `DisplayDialog`'s `--display-dialog-z-index`. A
|
|
62
|
+
toast is meant to float above ordinary page chrome (headers, sticky nav). If a toast appears behind
|
|
63
|
+
your site header, your header's z-index is at or above `999999`: raise the toast's token, or (more
|
|
64
|
+
robust) lower the header's z-index. If a `DisplayDialog` and a toast are visible at once, they tie
|
|
65
|
+
on paint order, an acceptable edge case for a transient notification.
|
|
9
66
|
|
|
10
|
-
|
|
11
|
-
toast is meant to float above ordinary page chrome (headers, sticky nav), so it needs to clear
|
|
12
|
-
whatever z-index a consumer's own layout uses for that. If a toast appears behind your site
|
|
13
|
-
header, your header's z-index is at or above `999999`; either raise the toast's token to match/
|
|
14
|
-
exceed it, or (more robust) lower the header's z-index to something more ordinary — very few
|
|
15
|
-
elements legitimately need to sit above a `dialog`/`alertdialog`.
|
|
67
|
+
## Motion
|
|
16
68
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
69
|
+
Toasts slide in and out over `behavior.revealDuration` (default `550ms`). Under
|
|
70
|
+
`prefers-reduced-motion: reduce` they fade instead, with no slide; the progress bar still runs
|
|
71
|
+
because it shows how long is left.
|
|
72
|
+
|
|
73
|
+
Auto-dismiss pauses while the pointer is over a toast, or while keyboard focus is on it or inside
|
|
74
|
+
it, and resumes with the time that was left (WCAG 2.2.2). The toast moves focus to itself when
|
|
75
|
+
shown; that only pauses it when the user was using a keyboard (`:focus-visible`), so mouse users
|
|
76
|
+
still get auto-dismiss.
|
|
77
|
+
|
|
78
|
+
## Global theming
|
|
20
79
|
|
|
21
80
|
```css
|
|
22
|
-
/*
|
|
23
|
-
:
|
|
24
|
-
--display-toast-
|
|
25
|
-
--display-toast-provider-
|
|
81
|
+
/* e.g. assets/styles/setup/07.components/display-toast.css */
|
|
82
|
+
:where(html) {
|
|
83
|
+
--display-toast-provider-max-width: min(36rem, 100% - 48px);
|
|
84
|
+
--display-toast-provider-progress-colour: var(--brand-accent);
|
|
26
85
|
}
|
|
27
86
|
```
|
|
28
87
|
|
|
29
|
-
---
|
|
30
|
-
|
|
31
88
|
## Local overrides
|
|
32
89
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
Set the tokens above on an element you own (a page or section class, or a class added with
|
|
91
|
+
`:style-class-passthrough`): they inherit down into the component. Keep the block **unlayered** (no
|
|
92
|
+
`@layer` wrapper) so it beats the library's `@layer components`. If your own file uses
|
|
93
|
+
`<style scoped>`, tokens set on your element still work, but selectors that reach inside the
|
|
94
|
+
component need `:deep()`. Patterns and examples: `.claude/skills/component-local-style-override.md`.
|
|
95
|
+
|
|
96
|
+
**Caveat:** both components teleport to `<body>`, so they aren't inside your page or section
|
|
97
|
+
elements and can't inherit tokens from them. Set toast tokens globally (`:root` or `:where(html)`),
|
|
98
|
+
or on `DisplayToast` itself via `:style-class-passthrough` (the class lands on the toast element).
|
|
99
|
+
`DisplayToastProvider` has no passthrough, so style it globally or with a `.display-toast-provider`
|
|
100
|
+
selector.
|
|
101
|
+
|
|
102
|
+
## Class passthrough
|
|
41
103
|
|
|
104
|
+
`DisplayToast`'s `styleClassPassthrough` adds classes to the teleported `.display-toast` element, so
|
|
105
|
+
it's the way to style one toast instance. `DisplayToastProvider` has none.
|