srcdev-nuxt-components 9.1.38 → 9.1.40

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 (29) hide show
  1. package/.claude/settings.json +4 -1
  2. package/.claude/settings.local.json +6 -1
  3. package/.claude/skills/components/carousel-flip.md +188 -0
  4. package/.claude/skills/index.md +3 -1
  5. package/.claude/skills/qa-panel.md +231 -0
  6. package/.claude/skills/testing-add-unit-test.md +32 -0
  7. package/app/components/01.atoms/{grid-stack → grids/grid-stack}/stories/GridStack.stories.ts +1 -1
  8. package/app/components/02.molecules/samaritan-prompt/SamaritanPrompt.vue +230 -0
  9. package/app/components/02.molecules/samaritan-prompt/SamaritanPromptMixed.vue +234 -0
  10. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPrompt.spec.ts +368 -0
  11. package/app/components/02.molecules/samaritan-prompt/tests/SamaritanPromptMixed.spec.ts +306 -0
  12. package/app/components/carousels/CarouselFlip.vue +211 -153
  13. package/app/components/carousels/stories/CarouselFlip.stories.ts +35 -0
  14. package/app/components/carousels/tests/CarouselFlip.spec.ts +38 -0
  15. package/app/composables/tests/useCancellableTimer.spec.ts +135 -0
  16. package/app/composables/useCancellableTimer.ts +42 -0
  17. package/app/pages/samaritan.vue +332 -0
  18. package/app/pages/ui/carousel-flip.vue +222 -17
  19. package/package.json +1 -1
  20. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/ScrollRevealFrame.vue +0 -0
  21. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/stories/ScrollRevealFrame.stories.ts +0 -0
  22. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/ScrollRevealFrame.spec.ts +0 -0
  23. /package/app/components/01.atoms/{scroll-reveal-frame → animations/scroll-reveal-frame}/tests/__snapshots__/ScrollRevealFrame.spec.ts.snap +0 -0
  24. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/ScrollRevealImage.vue +0 -0
  25. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/stories/ScrollRevealImage.stories.ts +0 -0
  26. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/ScrollRevealImage.spec.ts +0 -0
  27. /package/app/components/01.atoms/{scroll-reveal-image → animations/scroll-reveal-image}/tests/__snapshots__/ScrollRevealImage.spec.ts.snap +0 -0
  28. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/GridStack.vue +0 -0
  29. /package/app/components/01.atoms/{grid-stack → grids/grid-stack}/tests/GridStack.spec.ts +0 -0
@@ -0,0 +1,230 @@
1
+ <template>
2
+ <div :class="['samaritan-prompt', elementClasses]">
3
+ <div class="samaritan-prompt__content" :style="effect === 'word-pulse' ? { opacity: textOpacity } : undefined">
4
+ <div class="samaritan-prompt__stage">
5
+ <span class="samaritan-prompt__text">{{ displayText }}</span>
6
+ </div>
7
+ <div class="samaritan-prompt__underline"></div>
8
+ </div>
9
+ <span class="samaritan-prompt__cursor" :style="{ opacity: cursorOpacity }" aria-hidden="true">▲</span>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ interface Props {
15
+ messages: string[];
16
+ effect?: "typewriter" | "word-pulse";
17
+ typeSpeed?: number;
18
+ deleteSpeed?: number;
19
+ holdDuration?: number;
20
+ pauseDuration?: number;
21
+ wordDuration?: number;
22
+ fadeDuration?: number;
23
+ hideCursorInCycle?: boolean;
24
+ styleClassPassthrough?: string | string[];
25
+ }
26
+
27
+ const props = withDefaults(defineProps<Props>(), {
28
+ effect: "typewriter",
29
+ typeSpeed: 80,
30
+ deleteSpeed: 40,
31
+ holdDuration: 2000,
32
+ pauseDuration: 500,
33
+ wordDuration: 1200,
34
+ fadeDuration: 400,
35
+ hideCursorInCycle: true,
36
+ styleClassPassthrough: () => [],
37
+ });
38
+
39
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
40
+
41
+ const displayText = ref("");
42
+ const textOpacity = ref(1);
43
+ const cursorVisible = ref(true);
44
+ const fadeDurationCss = computed(() => `${props.fadeDuration}ms`);
45
+ const cursorOpacity = computed(() => (props.hideCursorInCycle && !cursorVisible.value ? 0 : 1));
46
+
47
+ const { wait, schedule, stop, start } = useCancellableTimer();
48
+
49
+ const startEffect = () => {
50
+ start();
51
+ displayText.value = "";
52
+ textOpacity.value = 1;
53
+ cursorVisible.value = true;
54
+ phase.value = "typing";
55
+ messageIndex.value = 0;
56
+ if (props.effect === "typewriter") {
57
+ schedule(typeTick, props.typeSpeed);
58
+ } else {
59
+ runWordPulse();
60
+ }
61
+ };
62
+
63
+ // --- Typewriter ---
64
+ type Phase = "typing" | "holding" | "deleting" | "pausing";
65
+ const phase = ref<Phase>("typing");
66
+ const messageIndex = ref(0);
67
+
68
+ const typeTick = () => {
69
+ const message = props.messages[messageIndex.value];
70
+ if (!message) return;
71
+
72
+ switch (phase.value) {
73
+ case "typing":
74
+ if (displayText.value.length === 0 && props.hideCursorInCycle) {
75
+ cursorVisible.value = false;
76
+ }
77
+ if (displayText.value.length < message.length) {
78
+ displayText.value = message.slice(0, displayText.value.length + 1);
79
+ schedule(typeTick, props.typeSpeed);
80
+ } else {
81
+ phase.value = "holding";
82
+ schedule(typeTick, props.holdDuration);
83
+ }
84
+ break;
85
+
86
+ case "holding":
87
+ phase.value = "deleting";
88
+ schedule(typeTick, props.deleteSpeed);
89
+ break;
90
+
91
+ case "deleting":
92
+ if (displayText.value.length > 0) {
93
+ displayText.value = displayText.value.slice(0, -1);
94
+ schedule(typeTick, props.deleteSpeed);
95
+ } else {
96
+ phase.value = "pausing";
97
+ if (props.hideCursorInCycle) cursorVisible.value = true;
98
+ schedule(typeTick, props.pauseDuration);
99
+ }
100
+ break;
101
+
102
+ case "pausing":
103
+ messageIndex.value = (messageIndex.value + 1) % props.messages.length;
104
+ phase.value = "typing";
105
+ schedule(typeTick, props.typeSpeed);
106
+ break;
107
+ }
108
+ };
109
+
110
+ // --- Word pulse ---
111
+ const runWordPulse = async () => {
112
+ try {
113
+ while (true) {
114
+ // Underline and cursor visible during the pre-cycle pause
115
+ await wait(props.pauseDuration);
116
+
117
+ if (props.hideCursorInCycle) cursorVisible.value = false;
118
+
119
+ // Fade out the underline before the first word appears
120
+ textOpacity.value = 0;
121
+ await wait(props.fadeDuration);
122
+
123
+ for (const message of props.messages) {
124
+ displayText.value = message;
125
+ await nextTick();
126
+ await wait(120);
127
+
128
+ textOpacity.value = 1;
129
+ await wait(props.wordDuration);
130
+
131
+ textOpacity.value = 0;
132
+ await wait(props.fadeDuration);
133
+ }
134
+
135
+ // Reset between cycles — underline and cursor visible again for next pause
136
+ displayText.value = "";
137
+ textOpacity.value = 1;
138
+ if (props.hideCursorInCycle) cursorVisible.value = true;
139
+ await nextTick();
140
+ }
141
+ } catch {
142
+ // component unmounted — exit cleanly
143
+ }
144
+ };
145
+
146
+ watch(
147
+ () => props.effect,
148
+ () => {
149
+ stop();
150
+ startEffect();
151
+ }
152
+ );
153
+
154
+ onMounted(startEffect);
155
+
156
+ onUnmounted(stop);
157
+ </script>
158
+
159
+ <style scoped>
160
+ @font-face {
161
+ font-family: "Mono MMM 5";
162
+ src: url("/fonts/monoMMM_5.ttf") format("truetype");
163
+ font-weight: normal;
164
+ font-style: normal;
165
+ font-display: swap;
166
+ }
167
+
168
+ .samaritan-prompt {
169
+ --_font-size: var(--samaritan-font-size, 2rem);
170
+ --_color-text: var(--samaritan-color-text, #ffffff);
171
+ --_color-underline: var(--samaritan-color-underline, #ffffff);
172
+ --_color-cursor: var(--samaritan-color-cursor, #cc0000);
173
+ --_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
174
+ --_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
175
+
176
+ display: flex;
177
+ flex-direction: column;
178
+ align-items: center;
179
+ row-gap: 0.6rem;
180
+ font-family: var(--_font-family);
181
+ font-size: var(--_font-size);
182
+ letter-spacing: var(--_letter-spacing);
183
+
184
+ .samaritan-prompt__content {
185
+ display: flex;
186
+ flex-direction: column;
187
+ align-items: center;
188
+ row-gap: 0.6rem;
189
+ width: 100%;
190
+ transition: opacity v-bind(fadeDurationCss) ease;
191
+
192
+ .samaritan-prompt__stage {
193
+ display: flex;
194
+ justify-content: center;
195
+ min-height: 1.2em;
196
+
197
+ .samaritan-prompt__text {
198
+ color: var(--_color-text);
199
+ white-space: nowrap;
200
+ text-transform: uppercase;
201
+ }
202
+ }
203
+
204
+ .samaritan-prompt__underline {
205
+ width: 100%;
206
+ min-width: 4ch;
207
+ height: 0.15rem;
208
+ background: var(--_color-underline);
209
+ }
210
+ }
211
+
212
+ .samaritan-prompt__cursor {
213
+ color: var(--_color-cursor);
214
+ font-size: 2.4rem;
215
+ line-height: 1;
216
+ animation: samaritan-pulse 2.5s ease-in-out infinite;
217
+ transition: opacity 400ms ease;
218
+ }
219
+ }
220
+
221
+ @keyframes samaritan-pulse {
222
+ 0%,
223
+ 100% {
224
+ color: var(--_color-cursor);
225
+ }
226
+ 50% {
227
+ color: #330000;
228
+ }
229
+ }
230
+ </style>
@@ -0,0 +1,234 @@
1
+ <template>
2
+ <div :class="['samaritan-prompt', elementClasses]">
3
+ <div class="samaritan-prompt__content" :style="{ opacity: textOpacity }">
4
+ <div class="samaritan-prompt__stage">
5
+ <span class="samaritan-prompt__text">{{ displayText }}</span>
6
+ </div>
7
+ <div class="samaritan-prompt__underline"></div>
8
+ </div>
9
+ <span class="samaritan-prompt__cursor" :style="{ opacity: cursorOpacity }" aria-hidden="true">▲</span>
10
+ </div>
11
+ </template>
12
+
13
+ <script setup lang="ts">
14
+ export interface MessageConfig {
15
+ text: string;
16
+ effect?: "typewriter" | "word-pulse";
17
+ typeSpeed?: number;
18
+ deleteSpeed?: number;
19
+ holdDuration?: number;
20
+ pauseDuration?: number;
21
+ wordDuration?: number;
22
+ fadeDuration?: number;
23
+ hideCursorInCycle?: boolean;
24
+ }
25
+
26
+ interface Props {
27
+ messageConfigs: MessageConfig[];
28
+ effect?: "typewriter" | "word-pulse";
29
+ typeSpeed?: number;
30
+ deleteSpeed?: number;
31
+ holdDuration?: number;
32
+ pauseDuration?: number;
33
+ wordDuration?: number;
34
+ fadeDuration?: number;
35
+ introDelay?: number;
36
+ hideCursorInCycle?: boolean;
37
+ styleClassPassthrough?: string | string[];
38
+ }
39
+
40
+ const props = withDefaults(defineProps<Props>(), {
41
+ effect: "typewriter",
42
+ typeSpeed: 80,
43
+ deleteSpeed: 40,
44
+ holdDuration: 7000,
45
+ pauseDuration: 1000,
46
+ wordDuration: 1200,
47
+ fadeDuration: 400,
48
+ introDelay: 2000,
49
+ hideCursorInCycle: true,
50
+ styleClassPassthrough: () => [],
51
+ });
52
+
53
+ const { elementClasses } = useStyleClassPassthrough(props.styleClassPassthrough);
54
+
55
+ const displayText = ref("");
56
+ const textOpacity = ref(1);
57
+ const cursorVisible = ref(true);
58
+ const activeFadeDuration = ref(props.fadeDuration);
59
+ const fadeDurationCss = computed(() => `${activeFadeDuration.value}ms`);
60
+ const cursorOpacity = computed(() => (cursorVisible.value ? 1 : 0));
61
+
62
+ const { wait, stop, start } = useCancellableTimer();
63
+
64
+ type ResolvedConfig = Required<MessageConfig>;
65
+
66
+ const resolveConfig = (msg: MessageConfig): ResolvedConfig => ({
67
+ text: msg.text,
68
+ effect: msg.effect ?? props.effect,
69
+ typeSpeed: msg.typeSpeed ?? props.typeSpeed,
70
+ deleteSpeed: msg.deleteSpeed ?? props.deleteSpeed,
71
+ holdDuration: msg.holdDuration ?? props.holdDuration,
72
+ pauseDuration: msg.pauseDuration ?? props.pauseDuration,
73
+ wordDuration: msg.wordDuration ?? props.wordDuration,
74
+ fadeDuration: msg.fadeDuration ?? props.fadeDuration,
75
+ hideCursorInCycle: msg.hideCursorInCycle ?? props.hideCursorInCycle,
76
+ });
77
+
78
+ const runTypewriter = async (config: ResolvedConfig) => {
79
+ const { text, typeSpeed, deleteSpeed, holdDuration, pauseDuration, hideCursorInCycle } = config;
80
+
81
+ if (hideCursorInCycle) cursorVisible.value = false;
82
+
83
+ for (let i = 1; i <= text.length; i++) {
84
+ displayText.value = text.slice(0, i);
85
+ await wait(typeSpeed);
86
+ }
87
+
88
+ await wait(holdDuration);
89
+
90
+ while (displayText.value.length > 0) {
91
+ displayText.value = displayText.value.slice(0, -1);
92
+ await wait(deleteSpeed);
93
+ }
94
+
95
+ if (hideCursorInCycle) cursorVisible.value = true;
96
+ await wait(pauseDuration);
97
+ };
98
+
99
+ const runWordPulse = async (config: ResolvedConfig) => {
100
+ const { text, fadeDuration, wordDuration, pauseDuration, hideCursorInCycle } = config;
101
+
102
+ activeFadeDuration.value = fadeDuration;
103
+ await nextTick();
104
+
105
+ if (hideCursorInCycle) cursorVisible.value = false;
106
+
107
+ textOpacity.value = 0;
108
+ await wait(fadeDuration);
109
+
110
+ displayText.value = text;
111
+ await nextTick();
112
+ await wait(120);
113
+
114
+ textOpacity.value = 1;
115
+ await wait(wordDuration);
116
+
117
+ textOpacity.value = 0;
118
+ await wait(fadeDuration);
119
+
120
+ displayText.value = "";
121
+ textOpacity.value = 1;
122
+ if (hideCursorInCycle) cursorVisible.value = true;
123
+ await nextTick();
124
+
125
+ await wait(pauseDuration);
126
+ };
127
+
128
+ const runLoop = async () => {
129
+ try {
130
+ while (true) {
131
+ if (props.introDelay > 0) await wait(props.introDelay);
132
+
133
+ // if (props.hideCursorInCycle) cursorVisible.value = false;
134
+
135
+ for (const msg of props.messageConfigs) {
136
+ const config = resolveConfig(msg);
137
+ if (config.effect === "typewriter") {
138
+ await runTypewriter(config);
139
+ } else {
140
+ await runWordPulse(config);
141
+ }
142
+ }
143
+
144
+ // if (props.hideCursorInCycle) cursorVisible.value = true;
145
+ }
146
+ } catch {
147
+ // component unmounted — exit cleanly
148
+ }
149
+ };
150
+
151
+ const startLoop = () => {
152
+ start();
153
+ displayText.value = "";
154
+ textOpacity.value = 1;
155
+ cursorVisible.value = true;
156
+ runLoop();
157
+ };
158
+
159
+ onMounted(startLoop);
160
+ onUnmounted(stop);
161
+ </script>
162
+
163
+ <style scoped>
164
+ @font-face {
165
+ font-family: "Mono MMM 5";
166
+ src: url("/fonts/monoMMM_5.ttf") format("truetype");
167
+ font-weight: normal;
168
+ font-style: normal;
169
+ font-display: swap;
170
+ }
171
+
172
+ .samaritan-prompt {
173
+ --_font-size: var(--samaritan-font-size, 2rem);
174
+ --_color-text: var(--samaritan-color-text, #ffffff);
175
+ --_color-underline: var(--samaritan-color-underline, #ffffff);
176
+ --_color-cursor: var(--samaritan-color-cursor, #cc0000);
177
+ --_font-family: var(--samaritan-font-family, "Mono MMM 5", "Nova Mono", "Courier New", monospace);
178
+ --_letter-spacing: var(--samaritan-letter-spacing, 0.08em);
179
+
180
+ display: flex;
181
+ flex-direction: column;
182
+ align-items: center;
183
+ row-gap: 0.6rem;
184
+ font-family: var(--_font-family);
185
+ font-size: var(--_font-size);
186
+ letter-spacing: var(--_letter-spacing);
187
+
188
+ .samaritan-prompt__content {
189
+ display: flex;
190
+ flex-direction: column;
191
+ align-items: center;
192
+ row-gap: 0.6rem;
193
+ width: 100%;
194
+ transition: opacity v-bind(fadeDurationCss) ease;
195
+
196
+ .samaritan-prompt__stage {
197
+ display: flex;
198
+ justify-content: center;
199
+ min-height: 1.2em;
200
+
201
+ .samaritan-prompt__text {
202
+ color: var(--_color-text);
203
+ white-space: nowrap;
204
+ text-transform: uppercase;
205
+ }
206
+ }
207
+
208
+ .samaritan-prompt__underline {
209
+ width: 100%;
210
+ min-width: 4ch;
211
+ height: 0.15rem;
212
+ background: var(--_color-underline);
213
+ }
214
+ }
215
+
216
+ .samaritan-prompt__cursor {
217
+ color: var(--_color-cursor);
218
+ font-size: 2.4rem;
219
+ line-height: 1;
220
+ animation: samaritan-pulse 2.5s ease-in-out infinite;
221
+ transition: opacity 400ms ease;
222
+ }
223
+ }
224
+
225
+ @keyframes samaritan-pulse {
226
+ 0%,
227
+ 100% {
228
+ color: var(--_color-cursor);
229
+ }
230
+ 50% {
231
+ color: #330000;
232
+ }
233
+ }
234
+ </style>