srcdev-nuxt-components 9.1.40 → 9.1.42

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.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: SamaritanPromptMixed
3
+ description: SamaritanPromptMixed animated text prompt — props, MessageConfig API, typewriter/word-pulse effects, accessibility (aria-live), CSS tokens, consumer styling
4
+ type: reference
5
+ ---
6
+
7
+ # SamaritanPromptMixed
8
+
9
+ ## Overview
10
+
11
+ `SamaritanPromptMixed` cycles through a list of messages using animated text effects. Each message can independently use either a `typewriter` effect (characters typed and deleted one-by-one) or a `word-pulse` effect (full text fades in, holds, then fades out). Effects and timing can be set globally via props and overridden per-message via `MessageConfig`.
12
+
13
+ The component uses `useCancellableTimer` for all async timing — the loop exits cleanly on `onUnmounted` without dangling promises.
14
+
15
+ ### Accessibility
16
+
17
+ - The visual animated content (`.samaritan-prompt__content`) carries `aria-hidden="true"` — screen readers never hear incremental characters.
18
+ - A visually hidden `aria-live="polite"` `aria-atomic="true"` span announces the complete message at the moment it is fully displayed:
19
+ - **Typewriter**: announced after all characters are typed (before the hold/delete phase).
20
+ - **Word-pulse**: announced when `textOpacity` reaches `1` (fade-in complete).
21
+ - `announcedText` is cleared to `""` before each deletion/fade-out, so the same message announced again in the next cycle fires a new announcement (value changes `"" → text`).
22
+ - `prefers-reduced-motion: reduce` disables the content fade transition and the cursor pulse animation via a CSS media query.
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Default | Description |
27
+ |------|------|---------|-------------|
28
+ | `messageConfigs` | `MessageConfig[]` | — | Array of message objects. **Required.** |
29
+ | `effect` | `"typewriter" \| "word-pulse"` | `"typewriter"` | Default effect for all messages. |
30
+ | `typeSpeed` | `number` | `80` | ms per character typed. |
31
+ | `deleteSpeed` | `number` | `40` | ms per character deleted. |
32
+ | `holdDuration` | `number` | `7000` | ms to hold the fully typed text before deleting. |
33
+ | `pauseDuration` | `number` | `1000` | ms pause after a message completes, before the next begins. |
34
+ | `wordDuration` | `number` | `1200` | ms the word-pulse text stays fully visible. |
35
+ | `fadeDuration` | `number` | `400` | ms for the word-pulse fade-in / fade-out CSS transition. |
36
+ | `introDelay` | `number` | `2000` | ms delay before the first message starts on mount. Set to `0` to start immediately. |
37
+ | `hideCursorInCycle` | `boolean` | `true` | Hide the cursor during text animation; show it during pause/hold. Per-message override available. |
38
+ | `styleClassPassthrough` | `string \| string[]` | `[]` | Extra classes applied to the root element. |
39
+
40
+ ## MessageConfig
41
+
42
+ Each entry in `messageConfigs` accepts:
43
+
44
+ ```typescript
45
+ interface MessageConfig {
46
+ text: string;
47
+ effect?: "typewriter" | "word-pulse"; // overrides global prop
48
+ typeSpeed?: number;
49
+ deleteSpeed?: number;
50
+ holdDuration?: number;
51
+ pauseDuration?: number;
52
+ wordDuration?: number;
53
+ fadeDuration?: number;
54
+ hideCursorInCycle?: boolean;
55
+ }
56
+ ```
57
+
58
+ Any field omitted falls back to the corresponding component prop.
59
+
60
+ ## Basic usage
61
+
62
+ ```vue
63
+ <SamaritanPromptMixed
64
+ :message-configs="[
65
+ { text: 'INITIALISING SYSTEM' },
66
+ { text: 'LOADING PROFILE' },
67
+ { text: 'ACCESS GRANTED' },
68
+ ]"
69
+ />
70
+ ```
71
+
72
+ ## Mixed effects
73
+
74
+ ```vue
75
+ <SamaritanPromptMixed
76
+ :message-configs="[
77
+ { text: 'INITIALISING SYSTEM', effect: 'typewriter', hold-duration: 3000 },
78
+ { text: 'STAND BY', effect: 'word-pulse', word-duration: 2000 },
79
+ { text: 'CONNECTED', effect: 'typewriter' },
80
+ ]"
81
+ />
82
+ ```
83
+
84
+ ## Fast intro, no delay
85
+
86
+ ```vue
87
+ <SamaritanPromptMixed
88
+ :intro-delay="0"
89
+ :type-speed="50"
90
+ :message-configs="[{ text: 'READY' }]"
91
+ />
92
+ ```
93
+
94
+ ## CSS custom properties
95
+
96
+ | Token | Default | Description |
97
+ |-------|---------|-------------|
98
+ | `--samaritan-font-size` | `2rem` | Text size. |
99
+ | `--samaritan-color-text` | `#ffffff` | Text colour. |
100
+ | `--samaritan-color-underline` | `#ffffff` | Underline bar colour. |
101
+ | `--samaritan-color-cursor` | `#cc0000` | Cursor colour (peak of pulse). |
102
+ | `--samaritan-color-cursor-off` | `transparent` | Cursor colour (trough of pulse). |
103
+ | `--samaritan-font-family` | `"Mono MMM 5", "Nova Mono", "Courier New", monospace` | Font stack. |
104
+ | `--samaritan-letter-spacing` | `0.08em` | Letter spacing. |
105
+
106
+ The custom font `Mono MMM 5` is loaded via `@font-face` inside the component's unscoped `<style>` block from `/fonts/monoMMM_5.ttf`.
107
+
108
+ ## Consumer styling
109
+
110
+ Use an unscoped style block scoped by a page or section wrapper. No `:deep()` needed.
111
+
112
+ ```vue
113
+ <style>
114
+ .my-page .samaritan-prompt {
115
+ --samaritan-font-size: 3.2rem;
116
+ --samaritan-color-text: #00ff88;
117
+ --samaritan-color-cursor: #ff4400;
118
+ }
119
+ </style>
120
+ ```
121
+
122
+ ## CSS classes
123
+
124
+ | Class | Notes |
125
+ |-------|-------|
126
+ | `.samaritan-prompt` | Root element. |
127
+ | `.samaritan-prompt__content` | Wraps text + underline. `aria-hidden="true"`. Opacity transitions on word-pulse effect. |
128
+ | `.samaritan-prompt__stage` | Centers the text span; min-height prevents layout shift. |
129
+ | `.samaritan-prompt__text` | The visible animated text. `text-transform: uppercase`. |
130
+ | `.samaritan-prompt__underline` | Horizontal rule below text. |
131
+ | `.samaritan-prompt__cursor` | `▲` glyph; pulses via `samaritan-pulse` keyframe. `aria-hidden="true"`. |
132
+ | `.samaritan-prompt__sr-text` | Visually hidden `aria-live` region for screen reader announcements. |
133
+
134
+ ## Notes
135
+
136
+ - The `introDelay` only applies at the start of the loop, not between messages. Between messages, `pauseDuration` applies.
137
+ - The cursor pulse animation uses the `samaritan-pulse` keyframe (defined in the component's global style). The cursor hides/shows during cycles via `cursorVisible` + opacity transition, not `display`.
138
+ - `prefers-reduced-motion: reduce` suppresses the opacity transition on `.samaritan-prompt__content` and the `animation` on `.samaritan-prompt__cursor`. The `aria-live` announcements continue to fire regardless.
139
+ - The `MessageConfig` type is exported from the component: `import type { MessageConfig } from 'srcdev-nuxt-components/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue'`.
@@ -77,7 +77,8 @@ Each skill is a single markdown file named `<area>-<task>.md`.
77
77
  ├── capture-qr-code.md — CaptureQrCode: live camera scanner, error state, visibility/route/KeepAlive lifecycle, media stream cleanup
78
78
  ├── decode-qr-code.md — DecodeQrCode: file picker + drag-and-drop image decoder, shared results list, CSS override points
79
79
  ├── data-grid.md — DataGrid: auto-fit responsive grid, $slots iteration, --data-grid-columns/gap tokens, semantic tag + aria
80
- └── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
80
+ ├── carousel-flip.md — CarouselFlip: FLIP-animated carousel, carouselDataIds slot API, buttonLayout variants (sides/controls-flanking/controls-grouped-right/overlay), CSS tokens
81
+ └── samaritan-prompt-mixed.md — SamaritanPromptMixed: animated text prompt, typewriter/word-pulse effects, MessageConfig API, aria-live accessibility, CSS tokens
81
82
  ```
82
83
 
83
84
  ## Skill file template
@@ -156,7 +156,7 @@ onMounted(startEffect);
156
156
  onUnmounted(stop);
157
157
  </script>
158
158
 
159
- <style scoped>
159
+ <style lang="css">
160
160
  @font-face {
161
161
  font-family: "Mono MMM 5";
162
162
  src: url("/fonts/monoMMM_5.ttf") format("truetype");
@@ -170,6 +170,7 @@ onUnmounted(stop);
170
170
  --_color-text: var(--samaritan-color-text, #ffffff);
171
171
  --_color-underline: var(--samaritan-color-underline, #ffffff);
172
172
  --_color-cursor: var(--samaritan-color-cursor, #cc0000);
173
+ --_color-cursor-off: var(--samaritan-color-cursor-off, transparent);
173
174
  --_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
174
175
  --_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
175
176
 
@@ -224,7 +225,7 @@ onUnmounted(stop);
224
225
  color: var(--_color-cursor);
225
226
  }
226
227
  50% {
227
- color: #330000;
228
+ color: var(--_color-cursor-off);
228
229
  }
229
230
  }
230
231
  </style>
@@ -1,12 +1,13 @@
1
1
  <template>
2
2
  <div :class="['samaritan-prompt', elementClasses]">
3
- <div class="samaritan-prompt__content" :style="{ opacity: textOpacity }">
3
+ <div class="samaritan-prompt__content" :style="{ opacity: textOpacity }" aria-hidden="true">
4
4
  <div class="samaritan-prompt__stage">
5
5
  <span class="samaritan-prompt__text">{{ displayText }}</span>
6
6
  </div>
7
7
  <div class="samaritan-prompt__underline"></div>
8
8
  </div>
9
9
  <span class="samaritan-prompt__cursor" :style="{ opacity: cursorOpacity }" aria-hidden="true">▲</span>
10
+ <span class="samaritan-prompt__sr-text" aria-live="polite" aria-atomic="true">{{ announcedText }}</span>
10
11
  </div>
11
12
  </template>
12
13
 
@@ -53,6 +54,7 @@ const props = withDefaults(defineProps<Props>(), {
53
54
  const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
54
55
 
55
56
  const displayText = ref("");
57
+ const announcedText = ref("");
56
58
  const textOpacity = ref(1);
57
59
  const cursorVisible = ref(true);
58
60
  const activeFadeDuration = ref(props.fadeDuration);
@@ -85,7 +87,9 @@ const runTypewriter = async (config: ResolvedConfig) => {
85
87
  await wait(typeSpeed);
86
88
  }
87
89
 
90
+ announcedText.value = text;
88
91
  await wait(holdDuration);
92
+ announcedText.value = "";
89
93
 
90
94
  while (displayText.value.length > 0) {
91
95
  displayText.value = displayText.value.slice(0, -1);
@@ -112,8 +116,10 @@ const runWordPulse = async (config: ResolvedConfig) => {
112
116
  await wait(120);
113
117
 
114
118
  textOpacity.value = 1;
119
+ announcedText.value = text;
115
120
  await wait(wordDuration);
116
121
 
122
+ announcedText.value = "";
117
123
  textOpacity.value = 0;
118
124
  await wait(fadeDuration);
119
125
 
@@ -160,7 +166,7 @@ onMounted(startLoop);
160
166
  onUnmounted(stop);
161
167
  </script>
162
168
 
163
- <style scoped>
169
+ <style lang="css">
164
170
  @font-face {
165
171
  font-family: "Mono MMM 5";
166
172
  src: url("/fonts/monoMMM_5.ttf") format("truetype");
@@ -174,6 +180,7 @@ onUnmounted(stop);
174
180
  --_color-text: var(--samaritan-color-text, #ffffff);
175
181
  --_color-underline: var(--samaritan-color-underline, #ffffff);
176
182
  --_color-cursor: var(--samaritan-color-cursor, #cc0000);
183
+ --_color-cursor-off: var(--samaritan-color-cursor-off, transparent);
177
184
  --_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
178
185
  --_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
179
186
 
@@ -220,6 +227,30 @@ onUnmounted(stop);
220
227
  animation: samaritan-pulse 2.5s ease-in-out infinite;
221
228
  transition: opacity 400ms ease;
222
229
  }
230
+
231
+ .samaritan-prompt__sr-text {
232
+ position: absolute;
233
+ width: 1px;
234
+ height: 1px;
235
+ padding: 0;
236
+ margin: -1px;
237
+ overflow: hidden;
238
+ clip: rect(0, 0, 0, 0);
239
+ white-space: nowrap;
240
+ border: 0;
241
+ }
242
+ }
243
+
244
+ @media (prefers-reduced-motion: reduce) {
245
+ .samaritan-prompt {
246
+ .samaritan-prompt__content {
247
+ transition: none;
248
+ }
249
+
250
+ .samaritan-prompt__cursor {
251
+ animation: none;
252
+ }
253
+ }
223
254
  }
224
255
 
225
256
  @keyframes samaritan-pulse {
@@ -228,7 +259,7 @@ onUnmounted(stop);
228
259
  color: var(--_color-cursor);
229
260
  }
230
261
  50% {
231
- color: #330000;
262
+ color: var(--_color-cursor-off);
232
263
  }
233
264
  }
234
265
  </style>
@@ -224,8 +224,8 @@ const qaHideCursorInCycle = ref(true);
224
224
 
225
225
  <style lang="css">
226
226
  .samaritan-page {
227
- background: #0a0a0a;
228
-
227
+ --samaritan-color-text: light-dark(black, white);
228
+ --samaritan-color-underline: light-dark(black, white);
229
229
  --samaritan-font-size: clamp(1.4rem, 2.5vw, 2.8rem);
230
230
  --samaritan-letter-spacing: 0.14em;
231
231
 
@@ -235,7 +235,7 @@ const qaHideCursorInCycle = ref(true);
235
235
  justify-content: center;
236
236
  min-block-size: 100dvh;
237
237
 
238
- &--mixed {
238
+ .samaritan-stage--mixed {
239
239
  border-top: 1px solid oklch(100% 0 0 / 0.08);
240
240
  }
241
241
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "srcdev-nuxt-components",
3
3
  "type": "module",
4
- "version": "9.1.40",
4
+ "version": "9.1.42",
5
5
  "main": "nuxt.config.ts",
6
6
  "types": "types.d.ts",
7
7
  "license": "MIT",