srcdev-nuxt-components 9.1.41 → 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'`.
|
package/.claude/skills/index.md
CHANGED
|
@@ -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
|
-
|
|
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
|
|
@@ -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
|
|
|
@@ -221,6 +227,30 @@ onUnmounted(stop);
|
|
|
221
227
|
animation: samaritan-pulse 2.5s ease-in-out infinite;
|
|
222
228
|
transition: opacity 400ms ease;
|
|
223
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
|
+
}
|
|
224
254
|
}
|
|
225
255
|
|
|
226
256
|
@keyframes samaritan-pulse {
|