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.
- package/.claude/skills/components/samaritan-prompt-mixed.md +139 -0
- package/.claude/skills/index.md +2 -1
- package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +3 -2
- package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +34 -3
- package/app/pages/samaritan.vue +3 -3
- package/package.json +1 -1
|
@@ -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
|
|
@@ -156,7 +156,7 @@ onMounted(startEffect);
|
|
|
156
156
|
onUnmounted(stop);
|
|
157
157
|
</script>
|
|
158
158
|
|
|
159
|
-
<style
|
|
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:
|
|
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
|
|
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:
|
|
262
|
+
color: var(--_color-cursor-off);
|
|
232
263
|
}
|
|
233
264
|
}
|
|
234
265
|
</style>
|
package/app/pages/samaritan.vue
CHANGED
|
@@ -224,8 +224,8 @@ const qaHideCursorInCycle = ref(true);
|
|
|
224
224
|
|
|
225
225
|
<style lang="css">
|
|
226
226
|
.samaritan-page {
|
|
227
|
-
|
|
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
|
-
|
|
238
|
+
.samaritan-stage--mixed {
|
|
239
239
|
border-top: 1px solid oklch(100% 0 0 / 0.08);
|
|
240
240
|
}
|
|
241
241
|
}
|