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.
Files changed (159) hide show
  1. package/.claude/skills/component-compliance-checklist.md +8 -2
  2. package/.claude/skills/component-dynamic-slots.md +3 -3
  3. package/.claude/skills/component-export-types.md +10 -16
  4. package/.claude/skills/component-naming.md +1 -1
  5. package/.claude/skills/components/alert-content.md +9 -1
  6. package/.claude/skills/components/breadcrumb.md +5 -0
  7. package/.claude/skills/components/container-glow.md +21 -8
  8. package/.claude/skills/components/display-avatar.md +13 -7
  9. package/.claude/skills/components/display-chip.md +37 -24
  10. package/.claude/skills/components/display-pill.md +58 -8
  11. package/.claude/skills/components/display-prompt.md +8 -5
  12. package/.claude/skills/components/display-toast.md +21 -4
  13. package/.claude/skills/components/glowing-border.md +20 -2
  14. package/.claude/skills/components/input-button.md +3 -0
  15. package/.claude/skills/components/responsive-header.md +59 -0
  16. package/.claude/skills/components/select-menu.md +54 -5
  17. package/.claude/skills/components/service-detail.md +32 -0
  18. package/.claude/skills/components/service-summary.md +8 -0
  19. package/.claude/skills/components/services-card-grid.md +5 -3
  20. package/.claude/skills/components/services-card.md +22 -12
  21. package/.claude/skills/components/site-header.md +2 -2
  22. package/.claude/skills/components/slider-gallery.md +10 -4
  23. package/.claude/skills/components/tabbed-content.md +300 -0
  24. package/.claude/skills/composable-anchor-scroll.md +16 -15
  25. package/.claude/skills/index.md +4 -5
  26. package/.claude/skills/storybook-add-story.md +53 -0
  27. package/.claude/skills/theming-status-tokens.md +60 -0
  28. package/.vscode/srcdev-component-display-avatar.code-snippets +2 -1
  29. package/.vscode/srcdev-component-display-chip.code-snippets +3 -2
  30. package/.vscode/srcdev-component-display-pill.code-snippets +10 -0
  31. package/.vscode/srcdev-component-responsive-header.code-snippets +16 -0
  32. package/.vscode/srcdev-component-select-menu.code-snippets +28 -0
  33. package/.vscode/srcdev-component-service-detail.code-snippets +1 -0
  34. package/.vscode/srcdev-component-service-summary.code-snippets +3 -1
  35. package/.vscode/srcdev-component-services-card.code-snippets +38 -36
  36. package/.vscode/srcdev-component-services-grids.code-snippets +2 -1
  37. package/.vscode/{srcdev-component-tabs-core.code-snippets → srcdev-component-tabbed-content.code-snippets} +35 -16
  38. package/app/assets/styles/setup/03.theming/_status.css +24 -0
  39. package/app/assets/styles/setup/03.theming/index.css +1 -0
  40. package/app/components/01.atoms/animations/container-glow/CONSUMER-STYLING.md +39 -35
  41. package/app/components/01.atoms/animations/container-glow/ContainerGlow.vue +25 -15
  42. package/app/components/01.atoms/animations/container-glow/stories/ContainerGlow.stories.ts +130 -24
  43. package/app/components/01.atoms/animations/container-glow/tests/ContainerGlow.spec.ts +45 -0
  44. package/app/components/01.atoms/animations/container-glow/tests/__snapshots__/ContainerGlow.spec.ts.snap +2 -2
  45. package/app/components/01.atoms/animations/glowing-border/CONSUMER-STYLING.md +76 -19
  46. package/app/components/01.atoms/animations/glowing-border/GlowingBorder.vue +12 -10
  47. package/app/components/01.atoms/animations/glowing-border/stories/GlowingBorder.stories.ts +201 -42
  48. package/app/components/01.atoms/animations/glowing-border/tests/GlowingBorder.spec.ts +15 -4
  49. package/app/components/01.atoms/animations/glowing-border/tests/__snapshots__/GlowingBorder.spec.ts.snap +1 -1
  50. package/app/components/01.atoms/display-pill/CONSUMER-STYLING.md +78 -9
  51. package/app/components/01.atoms/display-pill/DisplayPill.vue +59 -23
  52. package/app/components/01.atoms/display-pill/stories/DisplayPill.stories.ts +240 -16
  53. package/app/components/01.atoms/display-pill/tests/DisplayPill.spec.ts +43 -14
  54. package/app/components/01.atoms/display-pill/tests/__snapshots__/DisplayPill.spec.ts.snap +3 -3
  55. package/app/components/01.atoms/navigation/breadcrumb/Breadcrumb.vue +16 -4
  56. package/app/components/01.atoms/navigation/breadcrumb/CONSUMER-STYLING.md +28 -5
  57. package/app/components/01.atoms/navigation/breadcrumb/stories/Breadcrumb.stories.ts +94 -0
  58. package/app/components/01.atoms/navigation/breadcrumb/tests/Breadcrumb.spec.ts +27 -0
  59. package/app/components/01.atoms/tabbed-content/CONSUMER-STYLING.md +201 -0
  60. package/app/components/01.atoms/tabbed-content/TabbedContent.vue +750 -0
  61. package/app/components/01.atoms/tabbed-content/stories/TabbedContent.stories.ts +279 -0
  62. package/app/components/01.atoms/tabbed-content/tests/TabbedContent.spec.ts +816 -0
  63. package/app/components/02.molecules/alert-content/AlertContent.vue +11 -2
  64. package/app/components/02.molecules/alert-content/AlertContentInner.vue +19 -0
  65. package/app/components/02.molecules/alert-content/CONSUMER-STYLING.md +17 -4
  66. package/app/components/02.molecules/alert-content/stories/AlertContent.stories.ts +200 -17
  67. package/app/components/02.molecules/alert-content/tests/AlertContent.spec.ts +33 -0
  68. package/app/components/02.molecules/display-avatar/CONSUMER-STYLING.md +7 -5
  69. package/app/components/02.molecules/display-avatar/DisplayAvatar.vue +6 -2
  70. package/app/components/02.molecules/display-avatar/stories/DisplayAvatar.stories.ts +24 -3
  71. package/app/components/02.molecules/display-avatar/tests/DisplayAvatar.spec.ts +19 -0
  72. package/app/components/02.molecules/display-avatar/tests/__snapshots__/DisplayAvatar.spec.ts.snap +2 -1
  73. package/app/components/02.molecules/display-chip/CONSUMER-STYLING.md +52 -19
  74. package/app/components/02.molecules/display-chip/DisplayChip.vue +72 -54
  75. package/app/components/02.molecules/display-chip/stories/DisplayChip.stories.ts +103 -6
  76. package/app/components/02.molecules/display-chip/tests/DisplayChip.spec.ts +98 -29
  77. package/app/components/02.molecules/display-chip/tests/__snapshots__/DisplayChip.spec.ts.snap +8 -4
  78. package/app/components/02.molecules/prompt/CONSUMER-STYLING.md +9 -1
  79. package/app/components/02.molecules/prompt/DisplayPrompt.vue +2 -2
  80. package/app/components/02.molecules/prompt/stories/DisplayPrompt.stories.ts +208 -71
  81. package/app/components/02.molecules/prompt/tests/DisplayPrompt.spec.ts +19 -6
  82. package/app/components/02.molecules/select-menu/CONSUMER-STYLING.md +66 -8
  83. package/app/components/02.molecules/select-menu/SelectMenu.vue +141 -18
  84. package/app/components/02.molecules/select-menu/stories/SelectMenu.stories.ts +232 -2
  85. package/app/components/02.molecules/select-menu/tests/SelectMenu.spec.ts +151 -0
  86. package/app/components/02.molecules/select-menu/tests/__snapshots__/SelectMenu.spec.ts.snap +6 -2
  87. package/app/components/02.molecules/toast/CONSUMER-STYLING.md +91 -27
  88. package/app/components/02.molecules/toast/DisplayToast.vue +109 -50
  89. package/app/components/02.molecules/toast/DisplayToastProvider.vue +109 -45
  90. package/app/components/02.molecules/toast/stories/DisplayToast.stories.ts +23 -0
  91. package/app/components/02.molecules/toast/stories/DisplayToastProvider.stories.ts +73 -0
  92. package/app/components/02.molecules/toast/tests/DisplayToast.spec.ts +132 -18
  93. package/app/components/02.molecules/toast/tests/DisplayToastProvider.spec.ts +105 -39
  94. package/app/components/03.organisms/image-galleries/slider-gallery/CONSUMER-STYLING.md +19 -1
  95. package/app/components/03.organisms/image-galleries/slider-gallery/SliderGallery.vue +65 -7
  96. package/app/components/03.organisms/image-galleries/slider-gallery/stories/SliderGallery.stories.ts +201 -63
  97. package/app/components/03.organisms/image-galleries/slider-gallery/tests/SliderGallery.spec.ts +41 -2
  98. package/app/components/03.organisms/image-galleries/slider-gallery/tests/__snapshots__/SliderGallery.spec.ts.snap +6 -6
  99. package/app/components/03.organisms/responsive-header/CONSUMER-STYLING.md +42 -11
  100. package/app/components/03.organisms/responsive-header/NavigationItems.vue +60 -16
  101. package/app/components/03.organisms/responsive-header/ResponsiveHeader.vue +86 -24
  102. package/app/components/03.organisms/responsive-header/stories/ResponsiveHeader.stories.ts +52 -0
  103. package/app/components/03.organisms/responsive-header/tests/NavigationItems.spec.ts +20 -0
  104. package/app/components/03.organisms/responsive-header/tests/ResponsiveHeader.spec.ts +40 -0
  105. package/app/components/03.organisms/services/service-detail/CONSUMER-STYLING.md +92 -41
  106. package/app/components/03.organisms/services/service-detail/ServiceDetail.vue +198 -73
  107. package/app/components/03.organisms/services/service-detail/stories/ServiceDetail.stories.ts +157 -9
  108. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +58 -0
  109. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +5 -4
  110. package/app/components/03.organisms/services/service-summary/CONSUMER-STYLING.md +30 -0
  111. package/app/components/03.organisms/services/service-summary/ServiceSummary.vue +32 -8
  112. package/app/components/03.organisms/services/service-summary/stories/ServiceSummary.stories.ts +147 -0
  113. package/app/components/03.organisms/services/service-summary/tests/ServiceSummary.spec.ts +26 -0
  114. package/app/components/03.organisms/services/service-summary/tests/__snapshots__/ServiceSummary.spec.ts.snap +2 -2
  115. package/app/components/03.organisms/services/services-card/CONSUMER-STYLING.md +104 -63
  116. package/app/components/03.organisms/services/services-card/ServicesCard.vue +92 -66
  117. package/app/components/03.organisms/services/services-card/stories/ServicesCard.stories.ts +194 -27
  118. package/app/components/03.organisms/services/services-card/tests/ServicesCard.spec.ts +34 -8
  119. package/app/components/03.organisms/services/services-card/tests/__snapshots__/ServicesCard.spec.ts.snap +8 -8
  120. package/app/components/03.organisms/services/services-grids/CONSUMER-STYLING.md +9 -0
  121. package/app/components/03.organisms/services/services-grids/ServicesCardGrid.vue +2 -1
  122. package/app/components/03.organisms/services/services-grids/stories/ServiceSummaryGrid.stories.ts +46 -6
  123. package/app/components/03.organisms/services/services-grids/stories/ServicesCardGrid.stories.ts +89 -11
  124. package/app/components/03.organisms/services/services-grids/tests/ServicesCardGrid.spec.ts +8 -0
  125. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServiceSummaryGrid.spec.ts.snap +6 -6
  126. package/app/components/03.organisms/services/services-grids/tests/__snapshots__/ServicesCardGrid.spec.ts.snap +24 -24
  127. package/app/components/05.forms/input-button/CONSUMER-STYLING.md +8 -0
  128. package/app/components/05.forms/input-button/InputButton.vue +3 -1
  129. package/app/composables/tests/useAnchorScroll.spec.ts +8 -0
  130. package/app/composables/useAnchorScroll.ts +1 -1
  131. package/app/composables/useTabs.ts +148 -206
  132. package/app/types/components/display-chip.d.ts +8 -4
  133. package/app/types/components/display-toast.d.ts +2 -0
  134. package/app/types/components/index.ts +0 -1
  135. package/app/types/components/select-menu.d.ts +2 -0
  136. package/app/utils/pausableTimeout.ts +41 -0
  137. package/nuxt.config.ts +1 -1
  138. package/package.json +1 -1
  139. package/.claude/skills/components/site-navigation.md +0 -145
  140. package/.claude/skills/components/tab-navigation.md +0 -272
  141. package/.claude/skills/components/tabs-core.md +0 -187
  142. package/.vscode/srcdev-component-site-navigation.code-snippets +0 -38
  143. package/.vscode/srcdev-component-tab-navigation.code-snippets +0 -30
  144. package/app/components/01.atoms/navigation/tabs/CONSUMER-STYLING.md +0 -120
  145. package/app/components/01.atoms/navigation/tabs/TabsCore.vue +0 -272
  146. package/app/components/01.atoms/navigation/tabs/stories/TabsCore.stories.ts +0 -199
  147. package/app/components/01.atoms/navigation/tabs/tests/TabsCore.spec.ts +0 -274
  148. package/app/components/02.molecules/navigation/site-navigation/CONSUMER-STYLING.md +0 -109
  149. package/app/components/02.molecules/navigation/site-navigation/SiteNavigation.vue +0 -751
  150. package/app/components/02.molecules/navigation/site-navigation/stories/SiteNavigation.stories.ts +0 -356
  151. package/app/components/02.molecules/navigation/site-navigation/tests/SiteNavigation.spec.ts +0 -367
  152. package/app/components/02.molecules/navigation/site-navigation/tests/__snapshots__/SiteNavigation.spec.ts.snap +0 -30
  153. package/app/components/02.molecules/navigation/tab-navigation/CONSUMER-STYLING.md +0 -150
  154. package/app/components/02.molecules/navigation/tab-navigation/TabNavigation.vue +0 -518
  155. package/app/components/02.molecules/navigation/tab-navigation/stories/TabNavigation.stories.ts +0 -340
  156. package/app/components/02.molecules/navigation/tab-navigation/tests/TabNavigation.spec.ts +0 -339
  157. package/app/components/02.molecules/navigation/tab-navigation/tests/__snapshots__/TabNavigation.spec.ts.snap +0 -33
  158. package/app/composables/useNavCollapse.ts +0 -86
  159. package/app/types/components/nav-item.d.ts +0 -11
@@ -140,3 +140,76 @@ FullWidth.args = {
140
140
  theme: "warning",
141
141
  autoDismiss: false,
142
142
  };
143
+
144
+ const longUnbroken = "Donaudampfschifffahrtselektrizitätenhauptbetriebswerkbauunterbeamtengesellschaft";
145
+
146
+ const stressToasts = [
147
+ {
148
+ theme: "error" as const,
149
+ title: "Ihre Zahlung konnte leider nicht verarbeitet werden, bitte überprüfen Sie Ihre Kartendaten",
150
+ description:
151
+ "Die Bank hat die Transaktion abgelehnt. Bitte versuchen Sie es mit einer anderen Zahlungsmethode erneut oder wenden Sie sich an Ihre Bank, um weitere Informationen zu erhalten. ".repeat(
152
+ 2
153
+ ),
154
+ },
155
+ { theme: "warning" as const, title: longUnbroken, description: "https://example.com/" + "a-long-path-segment".repeat(6) },
156
+ { theme: "success" as const, title: "🎉 Emoji first", description: "<b>Not bold</b> <script>alert('x')</script>" },
157
+ { theme: "info" as const, title: "تم حفظ التغييرات بنجاح", description: "نص عربي طويل من اليمين إلى اليسار للتحقق من الاتجاه" },
158
+ { theme: "info" as const, title: "A", description: "" },
159
+ { theme: "info" as const, title: "", description: "Description only, no title." },
160
+ ];
161
+
162
+ export const StressTest: StoryFn<StoryArgs> = (args) => ({
163
+ components: { DisplayToastProvider },
164
+ setup() {
165
+ const { show, clear } = useToastQueue();
166
+ const queueStress = () => {
167
+ stressToasts.forEach(({ theme, title, description }) => {
168
+ show({
169
+ appearance: { theme },
170
+ behavior: { autoDismiss: args.autoDismiss, duration: args.duration },
171
+ content: { title, description, dismissLabel: "Benachrichtigung schließen" },
172
+ });
173
+ });
174
+ };
175
+ const flood = () => {
176
+ for (let index = 1; index <= 20; index++) {
177
+ show({ behavior: { autoDismiss: true, duration: 1500 }, content: { title: `Toast ${index} of 20` } });
178
+ }
179
+ };
180
+ return { args, queueStress, flood, clear };
181
+ },
182
+ template: `
183
+ <div style="padding: 2rem; min-height: 200px;">
184
+ <div style="display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: 1rem;">
185
+ <button @click="queueStress" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Queue worst-case toasts</button>
186
+ <button @click="flood" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Flood 20 toasts</button>
187
+ <button @click="clear" style="padding: 0.6rem 1.4rem; cursor: pointer; border: 1px solid #ccc; border-radius: 4px;">Clear Queue</button>
188
+ </div>
189
+ <DisplayToastProvider
190
+ :position="args.position"
191
+ :alignment="args.alignment"
192
+ :full-width="args.fullWidth"
193
+ :max-visible="args.maxVisible"
194
+ />
195
+ </div>
196
+ `,
197
+ });
198
+ StressTest.storyName = "Stress Test (Worst-Case Data)";
199
+ StressTest.args = {
200
+ maxVisible: 5,
201
+ autoDismiss: true,
202
+ duration: 8000,
203
+ };
204
+ StressTest.parameters = {
205
+ docs: {
206
+ description: {
207
+ story:
208
+ "Hostile toast content: long German copy, an unbroken word as the title, a long URL, emoji first, HTML-like text " +
209
+ "(must render as text), RTL, a one-character title, empty title and empty description, plus a flood of 20 queued toasts. " +
210
+ "Toasts are fixed to the viewport, so use Storybook's viewport toolbar (not a canvas width) to check narrow screens. " +
211
+ "Check that every toast stays inside the viewport and caps at 48rem on wide screens, unbroken text wraps, hovering a toast " +
212
+ "or tabbing into it freezes its progress bar and timer, and the flood drains in order without stacking past maxVisible.",
213
+ },
214
+ },
215
+ };
@@ -10,6 +10,9 @@ const { useAppConfigMock } = vi.hoisted(() => ({
10
10
 
11
11
  mockNuxtImport("useAppConfig", () => useAppConfigMock);
12
12
 
13
+ const mounted: { unmount: () => void }[] = [];
14
+ let focusVisibleSpy: { mockRestore: () => void } | null = null;
15
+
13
16
  // Helper: mount the toast and activate it so the teleported element is in the DOM.
14
17
  async function mountAndShow(props: Record<string, unknown> = {}, slots: Record<string, string> = {}) {
15
18
  const { config, styleClassPassthrough, ...rest } = props;
@@ -17,6 +20,7 @@ async function mountAndShow(props: Record<string, unknown> = {}, slots: Record<s
17
20
  props: { config, styleClassPassthrough } as Record<string, unknown>,
18
21
  slots,
19
22
  });
23
+ mounted.push(wrapper);
20
24
  await wrapper.setProps({ ...rest, modelValue: true });
21
25
  await nextTick();
22
26
  return wrapper;
@@ -27,7 +31,11 @@ function toast() {
27
31
  }
28
32
 
29
33
  describe("DisplayToast", () => {
34
+ // Unmount first: a leftover toast's pending timers would otherwise re-render into the cleared body.
30
35
  afterEach(() => {
36
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
37
+ focusVisibleSpy?.mockRestore();
38
+ focusVisibleSpy = null;
31
39
  document.body.innerHTML = "";
32
40
  });
33
41
 
@@ -50,9 +58,9 @@ describe("DisplayToast", () => {
50
58
  expect(toast()).not.toBeNull();
51
59
  });
52
60
 
53
- it("has the show class when activated", async () => {
61
+ it("has data-state='show' when activated", async () => {
54
62
  await mountAndShow();
55
- expect(toast()!.classList).toContain("show");
63
+ expect(toast()!.getAttribute("data-state")).toBe("show");
56
64
  });
57
65
 
58
66
  // ─── data-theme ───────────────────────────────────────────────────────────
@@ -83,26 +91,28 @@ describe("DisplayToast", () => {
83
91
  expect(toast()!.getAttribute("aria-live")).toBe(live);
84
92
  });
85
93
 
86
- // ─── Position classes ─────────────────────────────────────────────────────
94
+ // ─── Position hooks ───────────────────────────────────────────────────────
87
95
 
88
- it.each(["top", "bottom"] as const)("applies %s position class", async (position) => {
96
+ it.each(["top", "bottom"] as const)("sets data-position='%s'", async (position) => {
89
97
  await mountAndShow({ config: { appearance: { position } } });
90
- expect(toast()!.classList).toContain(position);
98
+ expect(toast()!.getAttribute("data-position")).toBe(position);
91
99
  });
92
100
 
93
- it.each(["left", "center", "right"] as const)("applies %s alignment class", async (alignment) => {
101
+ it.each(["left", "center", "right"] as const)("sets data-alignment='%s'", async (alignment) => {
94
102
  await mountAndShow({ config: { appearance: { alignment } } });
95
- expect(toast()!.classList).toContain(alignment);
103
+ expect(toast()!.getAttribute("data-alignment")).toBe(alignment);
96
104
  });
97
105
 
98
- it("applies full-width class when fullWidth is true", async () => {
99
- await mountAndShow({ config: { appearance: { fullWidth: true } } });
100
- expect(toast()!.classList).toContain("full-width");
106
+ it("sets data-alignment='full-width' when fullWidth is true, overriding alignment", async () => {
107
+ await mountAndShow({ config: { appearance: { fullWidth: true, alignment: "left" } } });
108
+ expect(toast()!.getAttribute("data-alignment")).toBe("full-width");
101
109
  });
102
110
 
103
- it("does not apply alignment class when fullWidth is true", async () => {
104
- await mountAndShow({ config: { appearance: { fullWidth: true, alignment: "left" } } });
105
- expect(toast()!.classList).not.toContain("left");
111
+ it("adds no generic position or state classes a consumer's CSS could match", async () => {
112
+ await mountAndShow({ config: { appearance: { position: "top", alignment: "center" } } });
113
+ for (const name of ["top", "bottom", "left", "center", "right", "show", "hide", "full-width"]) {
114
+ expect(toast()!.classList).not.toContain(name);
115
+ }
106
116
  });
107
117
 
108
118
  // ─── tabindex / aria-describedby ──────────────────────────────────────────
@@ -194,7 +204,7 @@ describe("DisplayToast", () => {
194
204
  await nextTick();
195
205
  toast()!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
196
206
  await nextTick();
197
- expect(toast()!.classList).toContain("hide");
207
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
198
208
  });
199
209
 
200
210
  // ─── Dismiss via close button ─────────────────────────────────────────────
@@ -205,7 +215,111 @@ describe("DisplayToast", () => {
205
215
  await nextTick();
206
216
  (document.querySelector("[data-test-id='alert-dismiss']") as HTMLElement).click();
207
217
  await nextTick();
208
- expect(toast()!.classList).toContain("hide");
218
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
219
+ });
220
+
221
+ // ─── Dismiss label ────────────────────────────────────────────────────────
222
+
223
+ it("passes config.content.dismissLabel to the close button's screen-reader text", async () => {
224
+ await mountAndShow({ config: { behavior: { autoDismiss: false }, content: { dismissLabel: "Schließen" } } });
225
+ expect(document.querySelector("[data-test-id='alert-dismiss']")!.textContent).toContain("Schließen");
226
+ });
227
+
228
+ // ─── Auto-dismiss pause ──────────────────────────────────────────────────
229
+
230
+ const autoConfig = { behavior: { autoDismiss: true, duration: 1000 } };
231
+
232
+ // jsdom matches :focus-visible whenever an element is focused; real browsers only do after keyboard input.
233
+ const originalMatches = Element.prototype.matches;
234
+ function setFocusVisible(keyboard: boolean) {
235
+ const spy = vi.spyOn(Element.prototype, "matches");
236
+ spy.mockImplementation(function (this: Element, selector: string) {
237
+ return selector === ":focus-visible" ? keyboard : originalMatches.call(this, selector);
238
+ });
239
+ focusVisibleSpy = spy;
240
+ }
241
+
242
+ it("auto-dismisses after the duration", async () => {
243
+ await mountAndShow({ config: autoConfig });
244
+ vi.advanceTimersByTime(1000);
245
+ await nextTick();
246
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
247
+ });
248
+
249
+ it("pauses auto-dismiss while hovered and resumes with the remaining time", async () => {
250
+ setFocusVisible(false);
251
+ await mountAndShow({ config: autoConfig });
252
+ vi.advanceTimersByTime(400);
253
+ toast()!.dispatchEvent(new Event("pointerenter"));
254
+ await nextTick();
255
+ expect(toast()!.hasAttribute("data-paused")).toBe(true);
256
+ vi.advanceTimersByTime(5000);
257
+ await nextTick();
258
+ expect(toast()!.getAttribute("data-state")).toBe("show");
259
+ toast()!.dispatchEvent(new Event("pointerleave"));
260
+ await nextTick();
261
+ expect(toast()!.hasAttribute("data-paused")).toBe(false);
262
+ vi.advanceTimersByTime(599);
263
+ await nextTick();
264
+ expect(toast()!.getAttribute("data-state")).toBe("show");
265
+ vi.advanceTimersByTime(1);
266
+ await nextTick();
267
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
268
+ });
269
+
270
+ it("pauses while focus is on something inside the toast", async () => {
271
+ await mountAndShow({ config: autoConfig }, { default: "<a href='#' class='inner-link'>Undo</a>" });
272
+ const link = document.querySelector(".inner-link") as HTMLElement;
273
+ link.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
274
+ await nextTick();
275
+ vi.advanceTimersByTime(5000);
276
+ await nextTick();
277
+ expect(toast()!.getAttribute("data-state")).toBe("show");
278
+ link.dispatchEvent(new FocusEvent("focusout", { bubbles: true, relatedTarget: null }));
279
+ await nextTick();
280
+ vi.advanceTimersByTime(1000);
281
+ await nextTick();
282
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
283
+ });
284
+
285
+ it("does not pause when the toast takes focus after mouse input", async () => {
286
+ setFocusVisible(false);
287
+ await mountAndShow({ config: autoConfig });
288
+ vi.advanceTimersByTime(100); // the toast focuses itself
289
+ await nextTick();
290
+ expect(document.activeElement).toBe(toast());
291
+ expect(toast()!.hasAttribute("data-paused")).toBe(false);
292
+ vi.advanceTimersByTime(900);
293
+ await nextTick();
294
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
295
+ });
296
+
297
+ it("pauses when the toast takes focus after keyboard input, until focus leaves", async () => {
298
+ setFocusVisible(true);
299
+ await mountAndShow({ config: autoConfig });
300
+ vi.advanceTimersByTime(100);
301
+ await nextTick();
302
+ expect(toast()!.hasAttribute("data-paused")).toBe(true);
303
+ vi.advanceTimersByTime(5000);
304
+ await nextTick();
305
+ expect(toast()!.getAttribute("data-state")).toBe("show");
306
+ (toast() as HTMLElement).blur();
307
+ await nextTick();
308
+ vi.advanceTimersByTime(900);
309
+ await nextTick();
310
+ expect(toast()!.getAttribute("data-state")).toBe("hide");
311
+ });
312
+
313
+ it("does not let an earlier show's timer dismiss a toast re-shown after a manual dismiss", async () => {
314
+ const wrapper = await mountAndShow({ config: autoConfig });
315
+ await wrapper.setProps({ modelValue: false });
316
+ vi.advanceTimersByTime(550);
317
+ await nextTick();
318
+ await wrapper.setProps({ modelValue: true });
319
+ await nextTick();
320
+ vi.advanceTimersByTime(600);
321
+ await nextTick();
322
+ expect(toast()!.getAttribute("data-state")).toBe("show");
209
323
  });
210
324
 
211
325
  // ─── styleClassPassthrough ────────────────────────────────────────────────
@@ -236,8 +350,8 @@ describe("DisplayToast", () => {
236
350
  it("uses hardcoded fallbacks when app.config has no displayToast key", async () => {
237
351
  await mountAndShow();
238
352
  expect(toast()!.getAttribute("data-theme")).toBe("info");
239
- expect(toast()!.classList).toContain("top");
240
- expect(toast()!.classList).toContain("right");
353
+ expect(toast()!.getAttribute("data-position")).toBe("top");
354
+ expect(toast()!.getAttribute("data-alignment")).toBe("right");
241
355
  expect(document.querySelector(".display-toast-progress")).not.toBeNull();
242
356
  });
243
357
 
@@ -260,7 +374,7 @@ describe("DisplayToast", () => {
260
374
  }));
261
375
  await mountAndShow({ config: { appearance: { theme: "warning", position: "top" } } });
262
376
  expect(toast()!.getAttribute("data-theme")).toBe("warning");
263
- expect(toast()!.classList).toContain("top");
377
+ expect(toast()!.getAttribute("data-position")).toBe("top");
264
378
  });
265
379
  });
266
380
  });
@@ -4,6 +4,15 @@ import { mountSuspended } from "@nuxt/test-utils/runtime";
4
4
  import DisplayToastProvider from "../DisplayToastProvider.vue";
5
5
  import { useToastQueue } from "~/composables/useToastQueue";
6
6
 
7
+ const mounted: { unmount: () => void }[] = [];
8
+
9
+ // Every provider shares the global queue, so a leftover one from an earlier test would run its own timers.
10
+ async function mountProvider(...args: Parameters<typeof mountSuspended<typeof DisplayToastProvider>>) {
11
+ const wrapper = await mountSuspended(...args);
12
+ mounted.push(wrapper);
13
+ return wrapper;
14
+ }
15
+
7
16
  function item() {
8
17
  return document.querySelector(".display-toast-provider-item");
9
18
  }
@@ -24,6 +33,7 @@ describe("DisplayToastProvider", () => {
24
33
  });
25
34
 
26
35
  afterEach(() => {
36
+ mounted.splice(0).forEach((wrapper) => wrapper.unmount());
27
37
  clear();
28
38
  document.body.innerHTML = "";
29
39
  });
@@ -31,47 +41,47 @@ describe("DisplayToastProvider", () => {
31
41
  // ─── Mount ────────────────────────────────────────────────────────────────
32
42
 
33
43
  it("mounts without error", async () => {
34
- const wrapper = await mountSuspended(DisplayToastProvider);
44
+ const wrapper = await mountProvider(DisplayToastProvider);
35
45
  expect(wrapper.vm).toBeTruthy();
36
46
  });
37
47
 
38
48
  it("renders the provider container", async () => {
39
- await mountSuspended(DisplayToastProvider);
49
+ await mountProvider(DisplayToastProvider);
40
50
  expect(provider()).not.toBeNull();
41
51
  });
42
52
 
43
53
  // ─── Empty state ──────────────────────────────────────────────────────────
44
54
 
45
55
  it("renders no items when queue is empty", async () => {
46
- await mountSuspended(DisplayToastProvider);
56
+ await mountProvider(DisplayToastProvider);
47
57
  expect(items().length).toBe(0);
48
58
  });
49
59
 
50
60
  // ─── Show / queue ─────────────────────────────────────────────────────────
51
61
 
52
62
  it("promotes a pending entry to visible on show()", async () => {
53
- await mountSuspended(DisplayToastProvider);
63
+ await mountProvider(DisplayToastProvider);
54
64
  show({ content: { text: "Hello" } });
55
65
  await nextTick();
56
66
  expect(items().length).toBe(1);
57
67
  });
58
68
 
59
69
  it("renders the toast message text", async () => {
60
- await mountSuspended(DisplayToastProvider);
70
+ await mountProvider(DisplayToastProvider);
61
71
  show({ content: { text: "Toast message" } });
62
72
  await nextTick();
63
73
  expect(document.querySelector(".alert-content-body")!.textContent).toContain("Toast message");
64
74
  });
65
75
 
66
76
  it("renders the toast title", async () => {
67
- await mountSuspended(DisplayToastProvider);
77
+ await mountProvider(DisplayToastProvider);
68
78
  show({ content: { title: "My Title" } });
69
79
  await nextTick();
70
80
  expect(document.querySelector("[data-test-id='alert-title']")!.textContent).toContain("My Title");
71
81
  });
72
82
 
73
83
  it("renders the toast description", async () => {
74
- await mountSuspended(DisplayToastProvider);
84
+ await mountProvider(DisplayToastProvider);
75
85
  show({ content: { description: "My description" } });
76
86
  await nextTick();
77
87
  expect(document.querySelector("[data-test-id='alert-content']")!.textContent).toContain("My description");
@@ -82,7 +92,7 @@ describe("DisplayToastProvider", () => {
82
92
  it.each(["info", "success", "warning", "error"] as const)(
83
93
  "sets data-theme='%s' from config",
84
94
  async (theme) => {
85
- await mountSuspended(DisplayToastProvider);
95
+ await mountProvider(DisplayToastProvider);
86
96
  show({ appearance: { theme } });
87
97
  await nextTick();
88
98
  expect(item()!.getAttribute("data-theme")).toBe(theme);
@@ -90,7 +100,7 @@ describe("DisplayToastProvider", () => {
90
100
  );
91
101
 
92
102
  it("defaults to data-theme='info'", async () => {
93
- await mountSuspended(DisplayToastProvider);
103
+ await mountProvider(DisplayToastProvider);
94
104
  show({});
95
105
  await nextTick();
96
106
  expect(item()!.getAttribute("data-theme")).toBe("info");
@@ -104,7 +114,7 @@ describe("DisplayToastProvider", () => {
104
114
  { theme: "warning" as const, role: "alert", live: "assertive" },
105
115
  { theme: "error" as const, role: "alert", live: "assertive" },
106
116
  ])("sets role=$role and aria-live=$live for theme='$theme'", async ({ theme, role, live }) => {
107
- await mountSuspended(DisplayToastProvider);
117
+ await mountProvider(DisplayToastProvider);
108
118
  show({ appearance: { theme } });
109
119
  await nextTick();
110
120
  expect(item()!.getAttribute("role")).toBe(role);
@@ -112,48 +122,56 @@ describe("DisplayToastProvider", () => {
112
122
  });
113
123
 
114
124
  it("sets aria-describedby on each item", async () => {
115
- await mountSuspended(DisplayToastProvider);
125
+ await mountProvider(DisplayToastProvider);
116
126
  show({});
117
127
  await nextTick();
118
128
  expect(item()!.getAttribute("aria-describedby")).not.toBeNull();
119
129
  });
120
130
 
121
131
  it("sets tabindex='0' on each item", async () => {
122
- await mountSuspended(DisplayToastProvider);
132
+ await mountProvider(DisplayToastProvider);
123
133
  show({});
124
134
  await nextTick();
125
135
  expect(item()!.getAttribute("tabindex")).toBe("0");
126
136
  });
127
137
 
128
- // ─── Position / alignment classes ────────────────────────────────────────
138
+ // ─── Position / alignment hooks ──────────────────────────────────────────
129
139
 
130
- it.each(["top", "bottom"] as const)("applies %s class to provider", async (position) => {
131
- await mountSuspended(DisplayToastProvider, { props: { position } });
132
- expect(provider()!.classList).toContain(position);
140
+ it.each(["top", "bottom"] as const)("sets data-position='%s' on the provider", async (position) => {
141
+ await mountProvider(DisplayToastProvider, { props: { position } });
142
+ expect(provider()!.getAttribute("data-position")).toBe(position);
133
143
  });
134
144
 
135
- it.each(["left", "center", "right"] as const)(
136
- "applies %s alignment class to provider",
137
- async (alignment) => {
138
- await mountSuspended(DisplayToastProvider, { props: { alignment } });
139
- expect(provider()!.classList).toContain(alignment);
140
- }
141
- );
145
+ it.each(["left", "center", "right"] as const)("sets data-alignment='%s' on the provider", async (alignment) => {
146
+ await mountProvider(DisplayToastProvider, { props: { alignment } });
147
+ expect(provider()!.getAttribute("data-alignment")).toBe(alignment);
148
+ });
149
+
150
+ it("sets data-alignment='full-width' when fullWidth is true, overriding alignment", async () => {
151
+ await mountProvider(DisplayToastProvider, { props: { fullWidth: true, alignment: "left" } });
152
+ expect(provider()!.getAttribute("data-alignment")).toBe("full-width");
153
+ });
142
154
 
143
- it("applies full-width class when fullWidth is true", async () => {
144
- await mountSuspended(DisplayToastProvider, { props: { fullWidth: true } });
145
- expect(provider()!.classList).toContain("full-width");
155
+ it("adds no generic position classes a consumer's CSS could match", async () => {
156
+ await mountProvider(DisplayToastProvider, { props: { position: "top", alignment: "center" } });
157
+ for (const name of ["top", "bottom", "left", "center", "right", "full-width"]) {
158
+ expect(provider()!.classList).not.toContain(name);
159
+ }
146
160
  });
147
161
 
148
- it("does not apply alignment class when fullWidth is true", async () => {
149
- await mountSuspended(DisplayToastProvider, { props: { fullWidth: true, alignment: "left" } });
150
- expect(provider()!.classList).not.toContain("left");
162
+ // ─── Dismiss label ────────────────────────────────────────────────────────
163
+
164
+ it("passes config.content.dismissLabel to the close button's screen-reader text", async () => {
165
+ await mountProvider(DisplayToastProvider);
166
+ show({ behavior: { autoDismiss: false }, content: { text: "Saved", dismissLabel: "Fermer" } });
167
+ await nextTick();
168
+ expect(document.querySelector("[data-test-id='alert-dismiss']")!.textContent).toContain("Fermer");
151
169
  });
152
170
 
153
171
  // ─── maxVisible ───────────────────────────────────────────────────────────
154
172
 
155
173
  it("shows only one item at a time when maxVisible=1", async () => {
156
- await mountSuspended(DisplayToastProvider, { props: { maxVisible: 1 } });
174
+ await mountProvider(DisplayToastProvider, { props: { maxVisible: 1 } });
157
175
  show({ content: { text: "First" } });
158
176
  show({ content: { text: "Second" } });
159
177
  await nextTick();
@@ -162,7 +180,7 @@ describe("DisplayToastProvider", () => {
162
180
  });
163
181
 
164
182
  it("shows up to maxVisible items simultaneously", async () => {
165
- await mountSuspended(DisplayToastProvider, { props: { maxVisible: 2 } });
183
+ await mountProvider(DisplayToastProvider, { props: { maxVisible: 2 } });
166
184
  show({ content: { text: "First" } });
167
185
  show({ content: { text: "Second" } });
168
186
  show({ content: { text: "Third" } });
@@ -173,7 +191,7 @@ describe("DisplayToastProvider", () => {
173
191
  // ─── Dismiss ──────────────────────────────────────────────────────────────
174
192
 
175
193
  it("removes item when close button is clicked", async () => {
176
- await mountSuspended(DisplayToastProvider);
194
+ await mountProvider(DisplayToastProvider);
177
195
  show({ behavior: { autoDismiss: false } });
178
196
  await nextTick();
179
197
  (document.querySelector("[data-test-id='alert-dismiss']") as HTMLElement).click();
@@ -182,7 +200,7 @@ describe("DisplayToastProvider", () => {
182
200
  });
183
201
 
184
202
  it("removes item when Escape is pressed", async () => {
185
- await mountSuspended(DisplayToastProvider);
203
+ await mountProvider(DisplayToastProvider);
186
204
  show({ behavior: { autoDismiss: false } });
187
205
  await nextTick();
188
206
  item()!.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));
@@ -193,7 +211,7 @@ describe("DisplayToastProvider", () => {
193
211
  // ─── Auto-dismiss timer ───────────────────────────────────────────────────
194
212
 
195
213
  it("auto-dismisses after duration", async () => {
196
- await mountSuspended(DisplayToastProvider);
214
+ await mountProvider(DisplayToastProvider);
197
215
  show({ behavior: { autoDismiss: true, duration: 3000 } });
198
216
  await nextTick();
199
217
  expect(items().length).toBe(1);
@@ -202,8 +220,56 @@ describe("DisplayToastProvider", () => {
202
220
  expect(items().length).toBe(0);
203
221
  });
204
222
 
223
+ it("pauses auto-dismiss while hovered and resumes with the remaining time", async () => {
224
+ await mountProvider(DisplayToastProvider);
225
+ show({ behavior: { autoDismiss: true, duration: 1000 } });
226
+ await nextTick();
227
+ vi.advanceTimersByTime(400);
228
+ item()!.dispatchEvent(new Event("pointerenter"));
229
+ await nextTick();
230
+ expect(item()!.hasAttribute("data-paused")).toBe(true);
231
+ vi.advanceTimersByTime(5000);
232
+ await nextTick();
233
+ expect(items().length).toBe(1);
234
+ item()!.dispatchEvent(new Event("pointerleave"));
235
+ await nextTick();
236
+ vi.advanceTimersByTime(599);
237
+ await nextTick();
238
+ expect(items().length).toBe(1);
239
+ vi.advanceTimersByTime(1);
240
+ await nextTick();
241
+ expect(items().length).toBe(0);
242
+ });
243
+
244
+ it("pauses while focus is on something inside a toast, and resumes when it leaves", async () => {
245
+ await mountProvider(DisplayToastProvider);
246
+ show({ behavior: { autoDismiss: true, duration: 1000 } });
247
+ await nextTick();
248
+ const inner = item()!.querySelector(".alert-content-body") as HTMLElement;
249
+ inner.dispatchEvent(new FocusEvent("focusin", { bubbles: true }));
250
+ await nextTick();
251
+ expect(item()!.hasAttribute("data-paused")).toBe(true);
252
+ vi.advanceTimersByTime(5000);
253
+ await nextTick();
254
+ expect(items().length).toBe(1);
255
+ inner.dispatchEvent(new FocusEvent("focusout", { bubbles: true, relatedTarget: null }));
256
+ await nextTick();
257
+ vi.advanceTimersByTime(1000);
258
+ await nextTick();
259
+ expect(items().length).toBe(0);
260
+ });
261
+
262
+ it("does not mark a non-auto-dismiss toast as paused", async () => {
263
+ await mountProvider(DisplayToastProvider);
264
+ show({ behavior: { autoDismiss: false } });
265
+ await nextTick();
266
+ item()!.dispatchEvent(new Event("pointerenter"));
267
+ await nextTick();
268
+ expect(item()!.hasAttribute("data-paused")).toBe(false);
269
+ });
270
+
205
271
  it("does not auto-dismiss when autoDismiss is false", async () => {
206
- await mountSuspended(DisplayToastProvider);
272
+ await mountProvider(DisplayToastProvider);
207
273
  show({ behavior: { autoDismiss: false } });
208
274
  await nextTick();
209
275
  vi.advanceTimersByTime(10000);
@@ -214,7 +280,7 @@ describe("DisplayToastProvider", () => {
214
280
  // ─── Queue progression ────────────────────────────────────────────────────
215
281
 
216
282
  it("promotes next pending entry after current is dismissed", async () => {
217
- await mountSuspended(DisplayToastProvider, { props: { maxVisible: 1 } });
283
+ await mountProvider(DisplayToastProvider, { props: { maxVisible: 1 } });
218
284
  show({ content: { text: "First" }, behavior: { autoDismiss: false } });
219
285
  show({ content: { text: "Second" }, behavior: { autoDismiss: false } });
220
286
  await nextTick();
@@ -228,14 +294,14 @@ describe("DisplayToastProvider", () => {
228
294
  // ─── Progress bar ─────────────────────────────────────────────────────────
229
295
 
230
296
  it("renders progress bar when autoDismiss is true", async () => {
231
- await mountSuspended(DisplayToastProvider);
297
+ await mountProvider(DisplayToastProvider);
232
298
  show({ behavior: { autoDismiss: true } });
233
299
  await nextTick();
234
300
  expect(document.querySelector(".display-toast-provider-progress")).not.toBeNull();
235
301
  });
236
302
 
237
303
  it("does not render progress bar when autoDismiss is false", async () => {
238
- await mountSuspended(DisplayToastProvider);
304
+ await mountProvider(DisplayToastProvider);
239
305
  show({ behavior: { autoDismiss: false } });
240
306
  await nextTick();
241
307
  expect(document.querySelector(".display-toast-provider-progress")).toBeNull();
@@ -245,7 +311,7 @@ describe("DisplayToastProvider", () => {
245
311
 
246
312
  it("removes all items on clear()", async () => {
247
313
  const { clear: clearQueue } = useToastQueue();
248
- await mountSuspended(DisplayToastProvider, { props: { maxVisible: 3 } });
314
+ await mountProvider(DisplayToastProvider, { props: { maxVisible: 3 } });
249
315
  show({});
250
316
  show({});
251
317
  await nextTick();
@@ -6,7 +6,8 @@
6
6
 
7
7
  | Token | Default | Controls |
8
8
  |---|---|---|
9
- | `--slider-gallery-height` | `100svh` | Gallery height (width is always `100vw`) |
9
+ | `--slider-gallery-height` | `100svh` | Gallery height |
10
+ | `--slider-gallery-width` | `100vw` | Gallery width. Set `100%` to fill a positioned container instead of the viewport |
10
11
  | `--slider-gallery-z-index` | `9999` | Stacking order (full-screen takeover) |
11
12
  | `--slider-gallery-accent` | `#f1683a` | Loading spinner and progress bar colour |
12
13
  | `--slider-gallery-loading-background` | `var(--page-bg)` | Loading overlay background |
@@ -25,6 +26,15 @@
25
26
  | `--slider-gallery-cta-background` | `#99999975` | See-more link background |
26
27
  | `--slider-gallery-cta-border-colour` | `#fff` | See-more link border |
27
28
  | `--slider-gallery-cta-colour` | `#fff` | See-more link text |
29
+ | `--slider-gallery-author-line-clamp` | `none` | Max lines of the stylist line |
30
+ | `--slider-gallery-title-line-clamp` | `none` | Max lines of the title |
31
+ | `--slider-gallery-topic-line-clamp` | `none` | Max lines of the category line |
32
+ | `--slider-gallery-description-line-clamp` | `none` | Max lines of the description |
33
+
34
+ Line-clamp tokens: `1` is single-line ellipsis, any other number caps the lines with an ellipsis
35
+ on the last, `none` shows everything. All slide text wraps long unbroken strings (`overflow-wrap: anywhere`).
36
+ The slide text is absolutely positioned, so very long copy can run into the thumbnails and arrows:
37
+ clamp the description if your CMS copy is unbounded.
28
38
 
29
39
  ### Thumbnails
30
40
 
@@ -43,6 +53,11 @@
43
53
  | `--slider-gallery-thumbnail-outline-offset-hover` | the resting offset | Outline gap on hover |
44
54
  | `--slider-gallery-thumbnail-border-radius` | `20px` | Thumbnail rounding |
45
55
  | `--slider-gallery-thumbnail-overlay` | `#0004` | Tint over thumbnail images |
56
+ | `--slider-gallery-thumbnail-title-line-clamp` | `2` | Max lines of a thumbnail title |
57
+ | `--slider-gallery-thumbnail-description-line-clamp` | `2` | Max lines of a thumbnail description |
58
+
59
+ The thumbnail clamps default to `2` because the card is a fixed size with its text anchored to the
60
+ bottom: uncapped, long text grows upwards and its first lines are cut off.
46
61
 
47
62
  ### Arrows
48
63
 
@@ -83,6 +98,9 @@ Private (not public API): `--_animation-duration` (from the `animationDuration`
83
98
  `--_arrow-border-*`/`--_arrow-outline-*` (resting values the hover tokens fall back to) and
84
99
  `--_arrows-top`/`-right`/`-width`/`-max-width` (the resolved, breakpoint-switched row position).
85
100
 
101
+ > Changed 2026-10-06: added `--slider-gallery-width` (was a fixed `100vw`) and the line-clamp tokens;
102
+ > slide, thumbnail, see-more and loading text now wrap unbroken strings.
103
+
86
104
  > Changed 2026-09-28: the thumbnail sizing/border/radius overrides used to be private names
87
105
  > (`--_thumbnailMobileWidth`, `--_thumbnailBorderRadius`, etc.) with no public token; colours were
88
106
  > hardcoded. The arrow icon is now sized by `--slider-gallery-arrow-icon-size` (width/height), separately from the button, and the `--_translate-x` icon nudge was removed. The wide thumbnail size now also applies to the image