srcdev-nuxt-components 9.6.19 → 9.6.20

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 (135) hide show
  1. package/.claude/rules/consumer/ids-from-useid.md +3 -2
  2. package/.claude/skills/component-compliance-checklist.md +5 -0
  3. package/.claude/skills/components/auto-grid.md +3 -1
  4. package/.claude/skills/components/capture-qr-code.md +9 -5
  5. package/.claude/skills/components/carousel-flip.md +41 -30
  6. package/.claude/skills/components/column-flow-grid.md +2 -0
  7. package/.claude/skills/components/dashboard-quad-grid.md +7 -2
  8. package/.claude/skills/components/dashboard-stats-grid.md +4 -0
  9. package/.claude/skills/components/decode-qr-code.md +7 -2
  10. package/.claude/skills/components/deep-expanding-menu-classic.md +31 -30
  11. package/.claude/skills/components/deep-expanding-menu.md +22 -21
  12. package/.claude/skills/components/grid-stack.md +2 -0
  13. package/.claude/skills/components/layout-grid-by-cols.md +2 -0
  14. package/.claude/skills/components/masonry-grid.md +9 -1
  15. package/.claude/skills/components/page-row.md +3 -0
  16. package/.claude/skills/components/samaritan-prompt.md +24 -8
  17. package/.claude/skills/components/site-header.md +16 -2
  18. package/.claude/skills/index.md +12 -12
  19. package/.claude/skills/storybook-add-story.md +25 -0
  20. package/.claude/skills/testing-add-unit-test.md +26 -0
  21. package/.vscode/srcdev-component-capture-decode-qr-code.code-snippets +11 -2
  22. package/.vscode/srcdev-component-carousel-flip.code-snippets +22 -4
  23. package/.vscode/srcdev-component-deep-expanding-menu-classic.code-snippets +32 -2
  24. package/.vscode/srcdev-component-deep-expanding-menu.code-snippets +31 -1
  25. package/.vscode/srcdev-component-samaritan-prompt.code-snippets +5 -4
  26. package/.vscode/srcdev-component-site-header.code-snippets +13 -1
  27. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/CONSUMER-STYLING.md +12 -2
  28. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/DashboardQuadGrid.vue +19 -17
  29. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/stories/DashboardQuadGrid.stories.ts +42 -0
  30. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-quad-grid/tests/DashboardQuadGrid.spec.ts +31 -44
  31. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/CONSUMER-STYLING.md +13 -1
  32. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/DashboardStatsGrid.vue +40 -31
  33. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/stories/DashboardStatsGrid.stories.ts +47 -0
  34. package/app/components/01.atoms/content-wrappers/layout-grid/dashboard-stats-grid/tests/DashboardStatsGrid.spec.ts +51 -53
  35. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/CONSUMER-STYLING.md +3 -0
  36. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/LayoutGridByCols.vue +4 -0
  37. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/stories/LayoutGridByCols.stories.ts +47 -2
  38. package/app/components/01.atoms/content-wrappers/layout-grid/layout-grid-by-cols/tests/LayoutGridByCols.spec.ts +36 -23
  39. package/app/components/01.atoms/grids/auto-grid/AutoGrid.vue +7 -3
  40. package/app/components/01.atoms/grids/auto-grid/CONSUMER-STYLING.md +1 -1
  41. package/app/components/01.atoms/grids/auto-grid/stories/AutoGrid.stories.ts +46 -1
  42. package/app/components/01.atoms/grids/auto-grid/tests/AutoGrid.spec.ts +40 -27
  43. package/app/components/01.atoms/grids/auto-grid/tests/__snapshots__/AutoGrid.spec.ts.snap +1 -1
  44. package/app/components/01.atoms/grids/column-flow-grid/CONSUMER-STYLING.md +9 -0
  45. package/app/components/01.atoms/grids/column-flow-grid/ColumnFlowGrid.vue +3 -2
  46. package/app/components/01.atoms/grids/column-flow-grid/stories/ColumnFlowGrid.stories.ts +46 -0
  47. package/app/components/01.atoms/grids/column-flow-grid/tests/ColumnFlowGrid.spec.ts +24 -11
  48. package/app/components/01.atoms/grids/grid-stack/CONSUMER-STYLING.md +3 -0
  49. package/app/components/01.atoms/grids/grid-stack/GridStack.vue +2 -0
  50. package/app/components/01.atoms/grids/grid-stack/stories/GridStack.stories.ts +41 -0
  51. package/app/components/01.atoms/grids/grid-stack/tests/GridStack.spec.ts +26 -13
  52. package/app/components/01.atoms/grids/masonry-grid/CONSUMER-STYLING.md +15 -0
  53. package/app/components/01.atoms/grids/masonry-grid/MasonryGrid.vue +51 -31
  54. package/app/components/01.atoms/grids/masonry-grid/stories/MasonryGrid.stories.ts +48 -0
  55. package/app/components/01.atoms/grids/masonry-grid/tests/MasonryGrid.spec.ts +84 -29
  56. package/app/components/01.atoms/page-row/CONSUMER-STYLING.md +6 -1
  57. package/app/components/01.atoms/page-row/PageRow.vue +16 -15
  58. package/app/components/01.atoms/page-row/stories/PageRow.stories.ts +23 -0
  59. package/app/components/01.atoms/page-row/tests/PageRow.spec.ts +6 -5
  60. package/app/components/02.molecules/navigation/deep-expanding-menu/CONSUMER-STYLING.md +63 -44
  61. package/app/components/02.molecules/navigation/deep-expanding-menu/DeepExpandingMenu.vue +74 -37
  62. package/app/components/02.molecules/navigation/deep-expanding-menu/stories/DeepExpandingMenu.stories.ts +139 -19
  63. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/DeepExpandingMenu.spec.ts +90 -27
  64. package/app/components/02.molecules/navigation/deep-expanding-menu/tests/__snapshots__/DeepExpandingMenu.spec.ts.snap +8 -8
  65. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/CONSUMER-STYLING.md +58 -26
  66. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/DeepExpandingMenuClassic.vue +113 -57
  67. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/stories/DeepExpandingMenuClassic.stories.ts +139 -19
  68. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/DeepExpandingMenuClassic.spec.ts +140 -22
  69. package/app/components/02.molecules/navigation/deep-expanding-menu-classic/tests/__snapshots__/DeepExpandingMenuClassic.spec.ts.snap +9 -9
  70. package/app/components/02.molecules/qr-code/CONSUMER-STYLING.md +22 -7
  71. package/app/components/02.molecules/qr-code/CaptureQrCode.vue +63 -71
  72. package/app/components/02.molecules/qr-code/DecodeQrCode.vue +47 -10
  73. package/app/components/02.molecules/qr-code/stories/CaptureQrCode.stories.ts +114 -26
  74. package/app/components/02.molecules/qr-code/stories/DecodeQrCode.stories.ts +115 -24
  75. package/app/components/02.molecules/qr-code/tests/CaptureQrCode.spec.ts +124 -51
  76. package/app/components/02.molecules/qr-code/tests/DecodeQrCode.spec.ts +74 -25
  77. package/app/components/02.molecules/samaritan-prompt/CONSUMER-STYLING.md +22 -10
  78. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +30 -12
  79. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -10
  80. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPrompt.stories.ts +55 -4
  81. package/app/components/02.molecules/samaritan-prompt/stories/SamaritanPromptMixed.stories.ts +67 -7
  82. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +113 -29
  83. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +92 -45
  84. package/app/components/03.organisms/image-galleries/carousel-flip/CONSUMER-STYLING.md +82 -29
  85. package/app/components/03.organisms/image-galleries/carousel-flip/CarouselFlip.vue +176 -113
  86. package/app/components/03.organisms/image-galleries/carousel-flip/stories/CarouselFlip.stories.ts +153 -23
  87. package/app/components/03.organisms/image-galleries/carousel-flip/tests/CarouselFlip.spec.ts +119 -63
  88. package/app/components/03.organisms/services/service-detail/tests/ServiceDetail.spec.ts +8 -8
  89. package/app/components/03.organisms/services/service-detail/tests/__snapshots__/ServiceDetail.spec.ts.snap +4 -4
  90. package/app/components/03.organisms/site-header/CONSUMER-STYLING.md +41 -23
  91. package/app/components/03.organisms/site-header/SiteHeader.vue +16 -21
  92. package/app/components/03.organisms/site-header/stories/SiteHeader.stories.ts +138 -0
  93. package/app/components/03.organisms/site-header/tests/SiteHeader.spec.ts +53 -26
  94. package/app/components/03.organisms/site-header/tests/__snapshots__/SiteHeader.spec.ts.snap +2 -2
  95. package/app/types/components/qr-code.d.ts +12 -0
  96. package/app/utils/graphemes.ts +8 -0
  97. package/package.json +1 -1
  98. package/app/components/01.atoms/animations/entry/.nuxt/app.config.mjs +0 -21
  99. package/app/components/01.atoms/animations/entry/.nuxt/components.d.ts +0 -66
  100. package/app/components/01.atoms/animations/entry/.nuxt/imports.d.ts +0 -30
  101. package/app/components/01.atoms/animations/entry/.nuxt/manifest/meta/test.json +0 -1
  102. package/app/components/01.atoms/animations/entry/.nuxt/types/app.config.d.ts +0 -35
  103. package/app/components/01.atoms/animations/entry/.nuxt/types/build.d.ts +0 -23
  104. package/app/components/01.atoms/animations/entry/.nuxt/types/builder-env.d.ts +0 -1
  105. package/app/components/01.atoms/animations/entry/.nuxt/types/components.d.ts +0 -71
  106. package/app/components/01.atoms/animations/entry/.nuxt/types/imports.d.ts +0 -386
  107. package/app/components/01.atoms/animations/entry/.nuxt/types/middleware.d.ts +0 -11
  108. package/app/components/01.atoms/animations/entry/.nuxt/types/modules.d.ts +0 -79
  109. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-layouts.d.ts +0 -17
  110. package/app/components/01.atoms/animations/entry/.nuxt/types/nitro-nuxt.d.ts +0 -64
  111. package/app/components/01.atoms/animations/entry/.nuxt/types/plugins.d.ts +0 -29
  112. package/app/components/01.atoms/animations/entry/.nuxt/types/runtime-config.d.ts +0 -32
  113. package/app/components/01.atoms/animations/entry/.nuxt/types/shared-imports.d.ts +0 -10
  114. package/app/components/01.atoms/animations/entry/.nuxt/types/vue-shim.d.ts +0 -0
  115. package/app/components/01.atoms/animations/entry/.nuxtrc +0 -1
  116. package/app/components/01.atoms/animations/entry/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
  117. package/app/components/01.atoms/animations/entry/node_modules/.cache/vite/client/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
  118. package/app/components/03.organisms/responsive-header/tests/.nuxt/app.config.mjs +0 -21
  119. package/app/components/03.organisms/responsive-header/tests/.nuxt/components.d.ts +0 -66
  120. package/app/components/03.organisms/responsive-header/tests/.nuxt/imports.d.ts +0 -30
  121. package/app/components/03.organisms/responsive-header/tests/.nuxt/manifest/meta/test.json +0 -1
  122. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/app.config.d.ts +0 -35
  123. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/build.d.ts +0 -23
  124. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/builder-env.d.ts +0 -1
  125. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/components.d.ts +0 -71
  126. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/imports.d.ts +0 -386
  127. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/middleware.d.ts +0 -11
  128. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/modules.d.ts +0 -79
  129. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-layouts.d.ts +0 -17
  130. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/nitro-nuxt.d.ts +0 -64
  131. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/plugins.d.ts +0 -29
  132. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/runtime-config.d.ts +0 -32
  133. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/shared-imports.d.ts +0 -10
  134. package/app/components/03.organisms/responsive-header/tests/.nuxt/types/vue-shim.d.ts +0 -0
  135. package/app/components/03.organisms/responsive-header/tests/node_modules/.cache/nuxt/chrome-workspace.json +0 -1
@@ -24,6 +24,7 @@
24
24
  </template>
25
25
 
26
26
  <script setup lang="ts">
27
+ import { usePreferredReducedMotion } from "@vueuse/core";
27
28
  import type { SamaritanPromptEffect } from "~/types/components/samaritan-prompt";
28
29
 
29
30
  interface Props {
@@ -66,6 +67,8 @@ const isPaused = ref(false);
66
67
  const fadeDurationCss = computed(() => `${props.fadeDuration}ms`);
67
68
  const cursorOpacity = computed(() => (props.hideCursorInCycle && !cursorVisible.value ? 0 : 1));
68
69
 
70
+ const reducedMotion = usePreferredReducedMotion();
71
+
69
72
  const timer = useCancellableTimer();
70
73
  const { wait, schedule, stop, start } = timer;
71
74
 
@@ -87,6 +90,7 @@ const startEffect = () => {
87
90
  cursorVisible.value = true;
88
91
  phase.value = "typing";
89
92
  messageIndex.value = 0;
93
+ typedCount.value = 0;
90
94
  if (props.effect === "typewriter") {
91
95
  schedule(typeTick, props.typeSpeed);
92
96
  } else {
@@ -98,18 +102,21 @@ const startEffect = () => {
98
102
  type Phase = "typing" | "holding" | "deleting" | "pausing";
99
103
  const phase = ref<Phase>("typing");
100
104
  const messageIndex = ref(0);
105
+ const typedCount = ref(0);
101
106
 
102
107
  const typeTick = () => {
103
108
  const message = props.messages[messageIndex.value];
104
- if (!message) return;
109
+ if (message === undefined) return;
110
+ const characters = splitGraphemes(message);
105
111
 
106
112
  switch (phase.value) {
107
113
  case "typing":
108
- if (displayText.value.length === 0 && props.hideCursorInCycle) {
114
+ if (typedCount.value === 0 && props.hideCursorInCycle) {
109
115
  cursorVisible.value = false;
110
116
  }
111
- if (displayText.value.length < message.length) {
112
- displayText.value = message.slice(0, displayText.value.length + 1);
117
+ if (typedCount.value < characters.length) {
118
+ typedCount.value = reducedMotion.value === "reduce" ? characters.length : typedCount.value + 1;
119
+ displayText.value = characters.slice(0, typedCount.value).join("");
113
120
  schedule(typeTick, props.typeSpeed);
114
121
  } else {
115
122
  phase.value = "holding";
@@ -125,8 +132,9 @@ const typeTick = () => {
125
132
  break;
126
133
 
127
134
  case "deleting":
128
- if (displayText.value.length > 0) {
129
- displayText.value = displayText.value.slice(0, -1);
135
+ if (typedCount.value > 0) {
136
+ typedCount.value = reducedMotion.value === "reduce" ? 0 : typedCount.value - 1;
137
+ displayText.value = characters.slice(0, typedCount.value).join("");
130
138
  schedule(typeTick, props.deleteSpeed);
131
139
  } else {
132
140
  phase.value = "pausing";
@@ -179,7 +187,7 @@ const runWordPulse = async () => {
179
187
  };
180
188
 
181
189
  watch(
182
- () => props.effect,
190
+ () => `${props.effect}:${JSON.stringify(props.messages)}`,
183
191
  () => {
184
192
  stop();
185
193
  startEffect();
@@ -205,6 +213,7 @@ onUnmounted(stop);
205
213
  display: flex;
206
214
  flex-direction: column;
207
215
  align-items: center;
216
+ max-inline-size: 100%;
208
217
  row-gap: var(--samaritan-prompt-cursor-gap, 0.6rem);
209
218
  font-family: var(--samaritan-prompt-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
210
219
  font-size: var(--samaritan-prompt-font-size, 2rem);
@@ -222,11 +231,20 @@ onUnmounted(stop);
222
231
  display: flex;
223
232
  justify-content: center;
224
233
  min-height: 1.2em;
234
+ max-inline-size: 100%;
235
+ min-inline-size: 0;
225
236
 
226
237
  .samaritan-prompt__text {
227
- color: var(--samaritan-prompt-text-colour, #ffffff);
228
- white-space: nowrap;
238
+ color: var(--samaritan-prompt-text-colour, var(--theme-text));
239
+ white-space: var(--samaritan-prompt-text-white-space, normal);
240
+ overflow-wrap: anywhere;
241
+ text-align: center;
229
242
  text-transform: var(--samaritan-prompt-text-transform, uppercase);
243
+ display: -webkit-box;
244
+ -webkit-box-orient: vertical;
245
+ overflow: hidden;
246
+ -webkit-line-clamp: var(--samaritan-prompt-text-line-clamp, none);
247
+ line-clamp: var(--samaritan-prompt-text-line-clamp, none);
230
248
  }
231
249
  }
232
250
 
@@ -234,12 +252,12 @@ onUnmounted(stop);
234
252
  width: 100%;
235
253
  min-width: 4ch;
236
254
  height: var(--samaritan-prompt-underline-height, 0.15rem);
237
- background: var(--samaritan-prompt-underline-colour, #ffffff);
255
+ background: var(--samaritan-prompt-underline-colour, var(--theme-text));
238
256
  }
239
257
  }
240
258
 
241
259
  .samaritan-prompt__cursor {
242
- color: var(--samaritan-prompt-cursor-colour, #cc0000);
260
+ color: var(--samaritan-prompt-cursor-colour, var(--theme-accent));
243
261
  font-size: var(--samaritan-prompt-cursor-size, 2.4rem);
244
262
  line-height: 1;
245
263
  animation: samaritan-prompt-pulse var(--samaritan-prompt-cursor-pulse-duration, 2.5s) ease-in-out infinite;
@@ -279,7 +297,7 @@ onUnmounted(stop);
279
297
  @keyframes samaritan-prompt-pulse {
280
298
  0%,
281
299
  100% {
282
- color: var(--samaritan-prompt-cursor-colour, #cc0000);
300
+ color: var(--samaritan-prompt-cursor-colour, var(--theme-accent));
283
301
  }
284
302
  50% {
285
303
  color: var(--samaritan-prompt-cursor-colour-off, transparent);
@@ -20,6 +20,7 @@
20
20
  </template>
21
21
 
22
22
  <script setup lang="ts">
23
+ import { usePreferredReducedMotion } from "@vueuse/core";
23
24
  import type { SamaritanPromptEffect, SamaritanPromptMessageConfig } from "~/types/components/samaritan-prompt";
24
25
 
25
26
  interface Props {
@@ -65,6 +66,10 @@ const activeFadeDuration = ref(props.fadeDuration);
65
66
  const fadeDurationCss = computed(() => `${activeFadeDuration.value}ms`);
66
67
  const cursorOpacity = computed(() => (cursorVisible.value ? 1 : 0));
67
68
 
69
+ const EMPTY_POLL_MS = 500;
70
+
71
+ const reducedMotion = usePreferredReducedMotion();
72
+
68
73
  const timer = useCancellableTimer();
69
74
  const { wait, stop, start } = timer;
70
75
 
@@ -97,8 +102,13 @@ const runTypewriter = async (config: ResolvedConfig) => {
97
102
 
98
103
  if (hideCursorInCycle) cursorVisible.value = false;
99
104
 
100
- for (let i = 1; i <= text.length; i++) {
101
- displayText.value = text.slice(0, i);
105
+ const characters = splitGraphemes(text);
106
+ const instant = reducedMotion.value === "reduce";
107
+ const typedLengths = instant ? [characters.length] : characters.map((_, i) => i + 1);
108
+ const deletedLengths = instant ? [0] : characters.map((_, i) => characters.length - 1 - i);
109
+
110
+ for (const length of typedLengths) {
111
+ displayText.value = characters.slice(0, length).join("");
102
112
  await wait(typeSpeed);
103
113
  }
104
114
 
@@ -106,8 +116,8 @@ const runTypewriter = async (config: ResolvedConfig) => {
106
116
  await wait(holdDuration);
107
117
  announcedText.value = "";
108
118
 
109
- while (displayText.value.length > 0) {
110
- displayText.value = displayText.value.slice(0, -1);
119
+ for (const length of deletedLengths) {
120
+ displayText.value = characters.slice(0, length).join("");
111
121
  await wait(deleteSpeed);
112
122
  }
113
123
 
@@ -149,6 +159,10 @@ const runWordPulse = async (config: ResolvedConfig) => {
149
159
  const runLoop = async () => {
150
160
  try {
151
161
  while (true) {
162
+ if (!props.messageConfigs.length) {
163
+ await wait(Math.max(props.introDelay, EMPTY_POLL_MS));
164
+ continue;
165
+ }
152
166
  if (props.introDelay > 0) await wait(props.introDelay);
153
167
 
154
168
  for (const msg of props.messageConfigs) {
@@ -183,7 +197,7 @@ onUnmounted(stop);
183
197
  src: url("/fonts/monoMMM_5.ttf") format("truetype");
184
198
  font-weight: normal;
185
199
  font-style: normal;
186
- font-display: optional;
200
+ font-display: swap;
187
201
  }
188
202
 
189
203
  @layer components {
@@ -191,6 +205,7 @@ onUnmounted(stop);
191
205
  display: flex;
192
206
  flex-direction: column;
193
207
  align-items: center;
208
+ max-inline-size: 100%;
194
209
  row-gap: var(--samaritan-prompt-cursor-gap, 0.6rem);
195
210
  font-family: var(--samaritan-prompt-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
196
211
  font-size: var(--samaritan-prompt-font-size, 2rem);
@@ -208,11 +223,20 @@ onUnmounted(stop);
208
223
  display: flex;
209
224
  justify-content: center;
210
225
  min-height: 1.2em;
226
+ max-inline-size: 100%;
227
+ min-inline-size: 0;
211
228
 
212
229
  .samaritan-prompt__text {
213
- color: var(--samaritan-prompt-text-colour, #ffffff);
214
- white-space: nowrap;
230
+ color: var(--samaritan-prompt-text-colour, var(--theme-text));
231
+ white-space: var(--samaritan-prompt-text-white-space, normal);
232
+ overflow-wrap: anywhere;
233
+ text-align: center;
215
234
  text-transform: var(--samaritan-prompt-text-transform, uppercase);
235
+ display: -webkit-box;
236
+ -webkit-box-orient: vertical;
237
+ overflow: hidden;
238
+ -webkit-line-clamp: var(--samaritan-prompt-text-line-clamp, none);
239
+ line-clamp: var(--samaritan-prompt-text-line-clamp, none);
216
240
  }
217
241
  }
218
242
 
@@ -220,12 +244,12 @@ onUnmounted(stop);
220
244
  width: 100%;
221
245
  min-width: 4ch;
222
246
  height: var(--samaritan-prompt-underline-height, 0.15rem);
223
- background: var(--samaritan-prompt-underline-colour, #ffffff);
247
+ background: var(--samaritan-prompt-underline-colour, var(--theme-text));
224
248
  }
225
249
  }
226
250
 
227
251
  .samaritan-prompt__cursor {
228
- color: var(--samaritan-prompt-cursor-colour, #cc0000);
252
+ color: var(--samaritan-prompt-cursor-colour, var(--theme-accent));
229
253
  font-size: var(--samaritan-prompt-cursor-size, 2.4rem);
230
254
  line-height: 1;
231
255
  animation: samaritan-prompt-pulse var(--samaritan-prompt-cursor-pulse-duration, 2.5s) ease-in-out infinite;
@@ -265,7 +289,7 @@ onUnmounted(stop);
265
289
  @keyframes samaritan-prompt-pulse {
266
290
  0%,
267
291
  100% {
268
- color: var(--samaritan-prompt-cursor-colour, #cc0000);
292
+ color: var(--samaritan-prompt-cursor-colour, var(--theme-accent));
269
293
  }
270
294
  50% {
271
295
  color: var(--samaritan-prompt-cursor-colour-off, transparent);
@@ -1,12 +1,15 @@
1
- import { computed } from "vue";
1
+ import { computed, ref } from "vue";
2
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
3
+ import type { MediaCanvas } from "~/types/components";
2
4
  import SamaritanPrompt from "../SamaritanPrompt.vue";
3
5
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
4
6
 
5
- // The default colours are white on the assumption of a dark page, so every story renders on a dark
6
- // stage. Keying on the serialised args restarts the loop when a timing control changes, so the
7
+ // Stories render the classic white-on-black look by setting the colour tokens on a dark stage (the
8
+ // defaults follow --theme-text and --theme-accent). Keying on the serialised args restarts the loop when a timing control changes, so the
7
9
  // new value is visible straight away instead of after the current cycle.
8
10
  const stageStyle =
9
- "display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem; background: #000; color: #fff;";
11
+ "display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem; background: #000; color: #fff; " +
12
+ "--samaritan-prompt-text-colour: #fff; --samaritan-prompt-underline-colour: #fff; --samaritan-prompt-cursor-colour: #cc0000;";
10
13
 
11
14
  const timingArgTypes = {
12
15
  typeSpeed: {
@@ -127,3 +130,51 @@ export const CustomCursorAndTokens: Story = {
127
130
  `,
128
131
  }),
129
132
  };
133
+
134
+ export const StressTest: Story = {
135
+ name: "Stress Test (Worst-Case Data)",
136
+ args: {
137
+ messages: [
138
+ "Die Überwachungssysteme sind vollständig aktiviert und melden ungewöhnliche Aktivitäten",
139
+ "https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own",
140
+ "",
141
+ "👍🏽 👨‍👩‍👧 ok",
142
+ "<b>Not bold</b>",
143
+ "مرحبا بالعالم",
144
+ "x",
145
+ ],
146
+ typeSpeed: 40,
147
+ deleteSpeed: 20,
148
+ holdDuration: 1500,
149
+ },
150
+ decorators: [
151
+ (story, context) => ({
152
+ components: { story, CanvasSwitcher },
153
+ setup() {
154
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
155
+ return { canvasName };
156
+ },
157
+ template: `
158
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
159
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
160
+ </div>
161
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
162
+ <story />
163
+ </div>
164
+ `,
165
+ }),
166
+ ],
167
+ parameters: {
168
+ layout: "fullscreen",
169
+ initialCanvas: "mobileCanvas",
170
+ docs: {
171
+ story: { inline: false, height: "520px" },
172
+ description: {
173
+ story:
174
+ "Hostile messages at every `CanvasSwitcher` width, typed fast so the cycle comes round quickly. Check that: the long German message and the unbroken URL wrap inside the canvas and stay centred; " +
175
+ "the empty message is skipped over rather than stopping the loop (until 2026-10-21 it halted the cycle); emoji with a skin tone and a family emoji type as whole characters, never half a glyph; " +
176
+ "`<b>` types as text; the right-to-left message reads correctly; a single character still shows. Try `effect: word-pulse` too, and turn on reduced motion in your OS: each message then appears whole instead of typing.",
177
+ },
178
+ },
179
+ },
180
+ };
@@ -1,12 +1,14 @@
1
- import { computed } from "vue";
1
+ import { computed, ref } from "vue";
2
+ import CanvasSwitcher from "../../../01.atoms/canvas-switcher/CanvasSwitcher.vue";
3
+ import type { MediaCanvas, SamaritanPromptMessageConfig } from "~/types/components";
2
4
  import SamaritanPromptMixed from "../SamaritanPromptMixed.vue";
3
5
  import type { Meta, StoryObj } from "@nuxtjs/storybook";
4
- import type { SamaritanPromptMessageConfig } from "~/types/components";
5
6
 
6
- // Dark stage because the default colours are white. Keying on the serialised args restarts the
7
+ // Dark stage with the classic white-on-black tokens (the defaults follow --theme-text and --theme-accent). Keying on the serialised args restarts the
7
8
  // loop when a control changes, so the new value is visible straight away.
8
9
  const stageStyle =
9
- "display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem; background: #000; color: #fff;";
10
+ "display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem; background: #000; color: #fff; " +
11
+ "--samaritan-prompt-text-colour: #fff; --samaritan-prompt-underline-colour: #fff; --samaritan-prompt-cursor-colour: #cc0000;";
10
12
 
11
13
  const messageConfigs: SamaritanPromptMessageConfig[] = [
12
14
  { text: "Good evening", effect: "typewriter", holdDuration: 2500 },
@@ -107,8 +109,8 @@ type Story = StoryObj<typeof SamaritanPromptMixed>;
107
109
 
108
110
  export const Default: Story = {};
109
111
 
110
- export const LightPage: Story = {
111
- name: "Light Page (token override)",
112
+ export const ThemeDefaults: Story = {
113
+ name: "Theme Defaults",
112
114
  render: (args) => ({
113
115
  components: { SamaritanPromptMixed },
114
116
  setup() {
@@ -116,9 +118,67 @@ export const LightPage: Story = {
116
118
  return { args, restartKey };
117
119
  },
118
120
  template: `
119
- <div style="display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem; background: #fff; --samaritan-prompt-text-colour: #000; --samaritan-prompt-underline-colour: #000;">
121
+ <div style="display: grid; place-items: center; min-block-size: 32rem; padding: 3.2rem;">
120
122
  <SamaritanPromptMixed :key="restartKey" v-bind="args" />
121
123
  </div>
122
124
  `,
123
125
  }),
126
+ parameters: {
127
+ docs: {
128
+ description: {
129
+ story: "No colour tokens set: text and underline follow `--theme-text`, the cursor `--theme-accent`.",
130
+ },
131
+ },
132
+ },
133
+ };
134
+
135
+ export const StressTest: Story = {
136
+ name: "Stress Test (Worst-Case Data)",
137
+ args: {
138
+ introDelay: 0,
139
+ messageConfigs: [
140
+ {
141
+ text: "Die Überwachungssysteme sind vollständig aktiviert und melden ungewöhnliche Aktivitäten",
142
+ effect: "typewriter",
143
+ typeSpeed: 30,
144
+ holdDuration: 1500,
145
+ },
146
+ { text: "https://example.com/a/very/long/unbroken/path/that/never/wraps/on/its/own", effect: "word-pulse" },
147
+ { text: "", effect: "typewriter", holdDuration: 300 },
148
+ { text: "👍🏽 👨‍👩‍👧 ok", effect: "typewriter", holdDuration: 1500 },
149
+ { text: "<b>Not bold</b>", effect: "word-pulse" },
150
+ { text: "مرحبا بالعالم", effect: "typewriter", holdDuration: 1500 },
151
+ { text: "x", effect: "typewriter", typeSpeed: 0, deleteSpeed: 0, holdDuration: 800, pauseDuration: 0 },
152
+ ],
153
+ },
154
+ decorators: [
155
+ (story, context) => ({
156
+ components: { story, CanvasSwitcher },
157
+ setup() {
158
+ const canvasName = ref<MediaCanvas>(context.parameters.initialCanvas ?? "fullWidthCanvas");
159
+ return { canvasName };
160
+ },
161
+ template: `
162
+ <div style="padding: 1.2rem 1.6rem; border-block-end: 1px solid currentColor;">
163
+ <CanvasSwitcher v-model:canvas-name="canvasName" />
164
+ </div>
165
+ <div :class="canvasName" style="margin-inline: auto; outline: 1px dashed currentColor;">
166
+ <story />
167
+ </div>
168
+ `,
169
+ }),
170
+ ],
171
+ parameters: {
172
+ layout: "fullscreen",
173
+ initialCanvas: "mobileCanvas",
174
+ docs: {
175
+ story: { inline: false, height: "520px" },
176
+ description: {
177
+ story:
178
+ "Hostile message configs at every `CanvasSwitcher` width, with `introDelay: 0`. Check that: the long German message and the unbroken URL wrap inside the canvas and stay centred; " +
179
+ "the empty message passes without stopping the loop; emoji with a skin tone and a family emoji type as whole characters; `<b>` renders as text; the right-to-left message reads correctly; " +
180
+ "the zero-speed single character still shows. Set `messageConfigs` to `[]` in Controls: the component idles instead of freezing the tab (until 2026-10-21 an empty list with `introDelay: 0` locked up the page).",
181
+ },
182
+ },
183
+ },
124
184
  };