@seams/voice-capture 0.0.0-stage → 0.67.0
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/LICENSE.md +201 -0
- package/README.md +174 -2
- package/dist/assets/NOTICE.txt +3 -0
- package/dist/assets/SILERO_LICENSE.txt +21 -0
- package/dist/assets/ort-wasm-simd-threaded.mjs +59 -0
- package/dist/assets/ort-wasm-simd-threaded.wasm +0 -0
- package/dist/assets/silero-vad.onnx +0 -0
- package/dist/assets/voice-activity-worklet.js +21 -0
- package/dist/browser/card.js +800 -0
- package/dist/browser/chunks/microphone-CU5gQG6w.js +1 -0
- package/dist/browser/chunks/ort.wasm.bundle.min-CWqlff_l.js +3 -0
- package/dist/browser/index.js +1 -0
- package/dist/esm/card/index.js +6 -0
- package/dist/esm/card/phraseCard.js +459 -0
- package/dist/esm/card/prompt.js +1074 -0
- package/dist/esm/card/ribbon.js +489 -0
- package/dist/esm/card/surface.js +224 -0
- package/dist/esm/card/types.js +0 -0
- package/dist/esm/card/visual.js +2 -0
- package/dist/esm/chunks/visual-Di3ojTED.js +1520 -0
- package/dist/esm/config.js +42 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/inference.js +193 -0
- package/dist/esm/microphone.js +428 -0
- package/dist/esm/microphoneInput.js +117 -0
- package/dist/esm/speechPause.js +33 -0
- package/dist/esm/voiceActivity.js +69 -0
- package/dist/esm/wav.js +36 -0
- package/dist/example/example.js +71 -0
- package/dist/example/index.html +29 -0
- package/dist/hosting.d.mts +4 -0
- package/dist/hosting.mjs +26 -0
- package/dist/types/card/index.d.ts +6 -0
- package/dist/types/card/phraseCard.d.ts +72 -0
- package/dist/types/card/prompt.d.ts +122 -0
- package/dist/types/card/ribbon.d.ts +62 -0
- package/dist/types/card/surface.d.ts +28 -0
- package/dist/types/card/types.d.ts +65 -0
- package/dist/types/card/visual.d.ts +25 -0
- package/dist/types/config.d.ts +47 -0
- package/dist/types/index.d.ts +7 -0
- package/dist/types/inference.d.ts +47 -0
- package/dist/types/microphone.d.ts +81 -0
- package/dist/types/microphoneInput.d.ts +19 -0
- package/dist/types/speechPause.d.ts +8 -0
- package/dist/types/voiceActivity.d.ts +15 -0
- package/dist/types/wav.d.ts +9 -0
- package/package.json +57 -4
|
@@ -0,0 +1,1074 @@
|
|
|
1
|
+
import { styleNonce } from "../config.js";
|
|
2
|
+
import { useSystemVoiceMicrophone } from "../microphoneInput.js";
|
|
3
|
+
import { t as VoiceVisual } from "../chunks/visual-Di3ojTED.js";
|
|
4
|
+
//#region src/card/prompt.ts
|
|
5
|
+
const CSS = `
|
|
6
|
+
.seams-voiceid-activation-surface { width: min(100%, 26.25rem); margin: 0 auto; background: transparent; }
|
|
7
|
+
/* Inside an operation's card the card is the frame: the prompt fills it edge to edge. */
|
|
8
|
+
.seams-voiceid-activation-surface.seams-voiceid-in-card { width: 100%; }
|
|
9
|
+
.seams-voiceid-in-card .seams-voiceid-prompt {
|
|
10
|
+
width: 100%;
|
|
11
|
+
padding-top: 0.75rem;
|
|
12
|
+
border: 0;
|
|
13
|
+
border-radius: 0;
|
|
14
|
+
background: transparent;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.seams-voiceid-prompt {
|
|
18
|
+
box-sizing: border-box;
|
|
19
|
+
position: relative;
|
|
20
|
+
width: min(100%, 26.25rem);
|
|
21
|
+
margin: 0 auto;
|
|
22
|
+
padding: 3.5rem 1.5rem 1.5rem;
|
|
23
|
+
display: grid;
|
|
24
|
+
grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) minmax(5rem, auto);
|
|
25
|
+
border: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
26
|
+
border-radius: var(--seams-shape-card, 16px);
|
|
27
|
+
background: var(--seams-colors-colorBackground, #192024);
|
|
28
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
29
|
+
/* While the card fills a taller frame (fillFrame), the spare height stays below the rows. */
|
|
30
|
+
align-content: start;
|
|
31
|
+
text-align: center;
|
|
32
|
+
font-family: var(--seams-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
|
|
33
|
+
-webkit-font-smoothing: antialiased;
|
|
34
|
+
}
|
|
35
|
+
/* A card that comes back after a Wallet confirm shows its rows only when the frame has eased to
|
|
36
|
+
its size (prompt.ts observeSize); until then they stay in the middle of the frame. */
|
|
37
|
+
.seams-voiceid-prompt[data-rows-held] > :not(.seams-voiceid-glow, .seams-voiceid-close, .seams-voiceid-progress-slot) {
|
|
38
|
+
opacity: 0 !important;
|
|
39
|
+
transition: none !important;
|
|
40
|
+
}
|
|
41
|
+
.seams-voiceid-prompt[data-frame-fill="center"] {
|
|
42
|
+
align-content: center;
|
|
43
|
+
}
|
|
44
|
+
.seams-voiceid-prompt {
|
|
45
|
+
transition: opacity 180ms var(--seams-ease-out, cubic-bezier(0.23, 1, 0.32, 1)), transform 180ms var(--seams-ease-out, cubic-bezier(0.23, 1, 0.32, 1));
|
|
46
|
+
}
|
|
47
|
+
@starting-style {
|
|
48
|
+
.seams-voiceid-prompt { opacity: 0; transform: translateY(4px); }
|
|
49
|
+
/* A card that continues another, or shows again after a Wallet confirm, has no entrance. */
|
|
50
|
+
.seams-voiceid-prompt.seams-voiceid-entered { opacity: 1; transform: none; }
|
|
51
|
+
/* The button it replaces was enabled a moment ago; this one dims from there. */
|
|
52
|
+
.seams-voiceid-prompt.seams-voiceid-entered .seams-voiceid-primary:disabled { opacity: 1; }
|
|
53
|
+
}
|
|
54
|
+
/* A card that holds focus itself (until its first control is ready, or after a confirm). */
|
|
55
|
+
.seams-voiceid-prompt[tabindex="-1"]:focus { outline: none; }
|
|
56
|
+
/* Each line owns its row. The download overlays rows 3-5; an auto-placed line would move to new rows. */
|
|
57
|
+
.seams-voiceid-prompt > h2 { grid-area: 1 / 1; }
|
|
58
|
+
.seams-voiceid-prompt > .seams-voiceid-phrase { grid-area: 2 / 1; }
|
|
59
|
+
.seams-voiceid-prompt > .seams-voiceid-ribbon { grid-area: 3 / 1; }
|
|
60
|
+
.seams-voiceid-prompt > .seams-voiceid-status { grid-area: 4 / 1; }
|
|
61
|
+
.seams-voiceid-progress-slot { grid-row: 5; grid-column: 1; min-width: 0; align-self: center; }
|
|
62
|
+
.seams-voiceid-progress-slot:empty { visibility: hidden; }
|
|
63
|
+
.seams-voiceid-activation-surface .seams-voiceid-record-slot { transition: opacity 300ms cubic-bezier(0.2, 0, 0, 1) 140ms, visibility 0s linear 0s; }
|
|
64
|
+
.seams-voiceid-activation-surface[data-preparation="loading"] .seams-voiceid-record-slot { opacity: 0; visibility: hidden; transition: none; }
|
|
65
|
+
@media (prefers-reduced-motion: reduce) {
|
|
66
|
+
.seams-voiceid-prompt { transform: none; transition: opacity 150ms ease; }
|
|
67
|
+
@starting-style { .seams-voiceid-prompt { transform: none; } }
|
|
68
|
+
.seams-voiceid-ribbon { transition: none !important; }
|
|
69
|
+
}
|
|
70
|
+
.seams-voiceid-prompt h2 {
|
|
71
|
+
font-size: 0.85rem;
|
|
72
|
+
line-height: 1.4;
|
|
73
|
+
font-weight: 500;
|
|
74
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
75
|
+
margin: 0.5rem 0 1rem;
|
|
76
|
+
}
|
|
77
|
+
.seams-voiceid-prompt.seams-voiceid-management {
|
|
78
|
+
display: flex;
|
|
79
|
+
flex-direction: column;
|
|
80
|
+
gap: 0.75rem;
|
|
81
|
+
padding: 1.25rem 1.25rem 1.375rem;
|
|
82
|
+
border-radius: var(--seams-shape-card, 1.5rem);
|
|
83
|
+
text-align: start;
|
|
84
|
+
font-size: 0.875rem;
|
|
85
|
+
line-height: 1.5;
|
|
86
|
+
}
|
|
87
|
+
.seams-voiceid-management-header {
|
|
88
|
+
display: flex;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: 0.25rem;
|
|
91
|
+
margin-inline-start: -0.5rem;
|
|
92
|
+
}
|
|
93
|
+
.seams-voiceid-management h2 {
|
|
94
|
+
margin: 0;
|
|
95
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
96
|
+
font-size: 1.375rem;
|
|
97
|
+
font-weight: 600;
|
|
98
|
+
letter-spacing: -0.02em;
|
|
99
|
+
line-height: 1.25;
|
|
100
|
+
}
|
|
101
|
+
.seams-voiceid-management .seams-voiceid-management-intro {
|
|
102
|
+
margin: -0.125rem 0 0.25rem;
|
|
103
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
104
|
+
}
|
|
105
|
+
.seams-voiceid-management button {
|
|
106
|
+
appearance: none;
|
|
107
|
+
margin: 0;
|
|
108
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
109
|
+
font: inherit;
|
|
110
|
+
transition: background-color 140ms ease;
|
|
111
|
+
}
|
|
112
|
+
.seams-voiceid-management button:disabled { opacity: 0.5; cursor: default; }
|
|
113
|
+
.seams-voiceid-management .seams-voiceid-management-back {
|
|
114
|
+
display: grid;
|
|
115
|
+
place-items: center;
|
|
116
|
+
width: 2.25rem;
|
|
117
|
+
height: 2.25rem;
|
|
118
|
+
padding: 0;
|
|
119
|
+
border: 0;
|
|
120
|
+
border-radius: var(--seams-shape-control, 10px);
|
|
121
|
+
background: transparent;
|
|
122
|
+
}
|
|
123
|
+
.seams-voiceid-management-enrollment {
|
|
124
|
+
display: grid;
|
|
125
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
126
|
+
align-items: center;
|
|
127
|
+
gap: 0.125rem 0.75rem;
|
|
128
|
+
padding: 0.75rem 0.875rem;
|
|
129
|
+
border-radius: 0.75rem;
|
|
130
|
+
background: var(--seams-colors-surface, #202b31);
|
|
131
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
132
|
+
}
|
|
133
|
+
.seams-voiceid-management-summary { text-align: end; }
|
|
134
|
+
.seams-voiceid-management-value { color: var(--seams-colors-textPrimary, #eff2f3); font-weight: 500; }
|
|
135
|
+
.seams-voiceid-management-enrollment-detail:not(:empty)::before { content: ' · '; }
|
|
136
|
+
.seams-voiceid-management-enrollment[data-enrollment="adapted"] > .seams-voiceid-management-summary {
|
|
137
|
+
grid-area: 2 / 1;
|
|
138
|
+
text-align: start;
|
|
139
|
+
font-size: 0.8125rem;
|
|
140
|
+
}
|
|
141
|
+
.seams-voiceid-management-enrollment > .seams-voiceid-management-reset { grid-area: 1 / 2 / 3; }
|
|
142
|
+
.seams-voiceid-management-enrollment[data-state] .seams-voiceid-management-value { display: none; }
|
|
143
|
+
.seams-voiceid-management-enrollment[data-state] .seams-voiceid-management-enrollment-detail::before { content: none; }
|
|
144
|
+
.seams-voiceid-management .seams-voiceid-management-reset {
|
|
145
|
+
padding: 0.375rem 0.75rem;
|
|
146
|
+
border: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
147
|
+
border-radius: var(--seams-shape-control, 10px);
|
|
148
|
+
background: transparent;
|
|
149
|
+
font-size: 0.8125rem;
|
|
150
|
+
font-weight: 600;
|
|
151
|
+
}
|
|
152
|
+
.seams-voiceid-management .seams-voiceid-management-action {
|
|
153
|
+
display: grid;
|
|
154
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
155
|
+
align-items: center;
|
|
156
|
+
gap: 0.75rem;
|
|
157
|
+
width: 100%;
|
|
158
|
+
min-height: 44px;
|
|
159
|
+
padding: 0.75rem 0.875rem;
|
|
160
|
+
border: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
161
|
+
border-radius: 0.75rem;
|
|
162
|
+
background: transparent;
|
|
163
|
+
text-align: start;
|
|
164
|
+
}
|
|
165
|
+
.seams-voiceid-management-action > svg { color: var(--seams-colors-textSecondary, #9cabb3); }
|
|
166
|
+
.seams-voiceid-management-action-heading {
|
|
167
|
+
display: flex;
|
|
168
|
+
flex-wrap: wrap;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 0.25rem 0.5rem;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
}
|
|
173
|
+
.seams-voiceid-management-chip {
|
|
174
|
+
padding: 0.125rem 0.5rem;
|
|
175
|
+
border-radius: 6px;
|
|
176
|
+
background: color-mix(in srgb, var(--seams-colors-textPrimary, #eff2f3), transparent 93%);
|
|
177
|
+
font-size: 0.75rem;
|
|
178
|
+
font-weight: 500;
|
|
179
|
+
}
|
|
180
|
+
.seams-voiceid-management-action-detail {
|
|
181
|
+
display: block;
|
|
182
|
+
margin-top: 0.125rem;
|
|
183
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
184
|
+
font-size: 0.8125rem;
|
|
185
|
+
}
|
|
186
|
+
.seams-voiceid-management .seams-voiceid-management-action[data-state="pending"] {
|
|
187
|
+
opacity: 1;
|
|
188
|
+
border-color: var(--seams-colors-borderHover, #46545b);
|
|
189
|
+
background: var(--seams-colors-surface, #202b31);
|
|
190
|
+
}
|
|
191
|
+
.seams-voiceid-management-action[data-state="pending"] > svg { visibility: hidden; }
|
|
192
|
+
.seams-voiceid-management [data-state="pending"] .seams-voiceid-management-action-detail,
|
|
193
|
+
.seams-voiceid-management [data-state="pending"] .seams-voiceid-management-enrollment-detail {
|
|
194
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
195
|
+
}
|
|
196
|
+
.seams-voiceid-management [data-state="error"] .seams-voiceid-management-action-detail,
|
|
197
|
+
.seams-voiceid-management [data-state="error"] .seams-voiceid-management-enrollment-detail {
|
|
198
|
+
color: var(--seams-colors-error, oklch(0.74 0.166 19));
|
|
199
|
+
}
|
|
200
|
+
.seams-voiceid-management .seams-voiceid-management-section {
|
|
201
|
+
margin: 0.5rem 0 -0.25rem;
|
|
202
|
+
padding-inline-start: 0.125rem;
|
|
203
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
204
|
+
font-size: 0.6875rem;
|
|
205
|
+
font-weight: 600;
|
|
206
|
+
letter-spacing: 0.08em;
|
|
207
|
+
text-transform: uppercase;
|
|
208
|
+
}
|
|
209
|
+
/* The status line is read by assistive technology; a message for a hidden row is also shown. */
|
|
210
|
+
.seams-voiceid-prompt.seams-voiceid-management > [role="status"] {
|
|
211
|
+
position: absolute;
|
|
212
|
+
width: 1px;
|
|
213
|
+
height: 1px;
|
|
214
|
+
margin: -1px;
|
|
215
|
+
overflow: hidden;
|
|
216
|
+
clip-path: inset(50%);
|
|
217
|
+
white-space: nowrap;
|
|
218
|
+
}
|
|
219
|
+
.seams-voiceid-prompt.seams-voiceid-management > [role="status"][data-placement="dialog"] {
|
|
220
|
+
position: relative;
|
|
221
|
+
width: auto;
|
|
222
|
+
height: auto;
|
|
223
|
+
margin: 0;
|
|
224
|
+
overflow: visible;
|
|
225
|
+
clip-path: none;
|
|
226
|
+
white-space: normal;
|
|
227
|
+
color: var(--seams-colors-error, oklch(0.74 0.166 19));
|
|
228
|
+
font-size: 0.8125rem;
|
|
229
|
+
overflow-wrap: anywhere;
|
|
230
|
+
}
|
|
231
|
+
[dir='rtl'] .seams-voiceid-management svg { transform: scaleX(-1); }
|
|
232
|
+
@media (hover: hover) {
|
|
233
|
+
.seams-voiceid-management button:hover:not(:disabled) {
|
|
234
|
+
background: var(--seams-colors-surface2, var(--seams-colors-surface, #202b31));
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
.seams-voiceid-prompt .seams-voiceid-phrase {
|
|
238
|
+
font-size: clamp(1.75rem, 6vw, 2.25rem);
|
|
239
|
+
font-weight: 600;
|
|
240
|
+
line-height: 1.17;
|
|
241
|
+
letter-spacing: -0.035em;
|
|
242
|
+
text-wrap: balance;
|
|
243
|
+
max-width: 21rem;
|
|
244
|
+
align-self: center;
|
|
245
|
+
margin: 0 auto;
|
|
246
|
+
transition: opacity 180ms var(--seams-ease-out, cubic-bezier(0.23, 1, 0.32, 1)), color 300ms ease;
|
|
247
|
+
}
|
|
248
|
+
.seams-voiceid-ribbon {
|
|
249
|
+
display: block;
|
|
250
|
+
width: 308px;
|
|
251
|
+
max-width: 100%;
|
|
252
|
+
height: 64px;
|
|
253
|
+
margin: auto;
|
|
254
|
+
overflow: visible;
|
|
255
|
+
}
|
|
256
|
+
.seams-voiceid-ribbon .seams-voiceid-ribbon-check { stroke: var(--seams-colors-colorBackground, #192024); }
|
|
257
|
+
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-success-headline {
|
|
258
|
+
display: grid;
|
|
259
|
+
align-content: end;
|
|
260
|
+
font-size: 18px;
|
|
261
|
+
letter-spacing: normal;
|
|
262
|
+
line-height: 1.4;
|
|
263
|
+
}
|
|
264
|
+
.seams-voiceid-prompt .seams-voiceid-status.seams-voiceid-success-message { font-size: 0.8125rem; }
|
|
265
|
+
/* The glow is decorative and cannot intercept record, retry or cancel controls. */
|
|
266
|
+
.seams-voiceid-glow { position: absolute; inset: 0; pointer-events: none; border-radius: inherit; z-index: 0; }
|
|
267
|
+
.seams-voiceid-beam { width: 100%; height: 100%; border-radius: inherit; }
|
|
268
|
+
.seams-voiceid-prompt > :not(.seams-voiceid-glow) { position: relative; z-index: 1; }
|
|
269
|
+
.seams-voiceid-prompt > .seams-voiceid-close { position: absolute; }
|
|
270
|
+
.seams-voiceid-prompt .seams-voiceid-status {
|
|
271
|
+
font-size: 0.75rem;
|
|
272
|
+
line-height: 1.5;
|
|
273
|
+
min-height: 3.375rem;
|
|
274
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
275
|
+
margin: 0;
|
|
276
|
+
}
|
|
277
|
+
.seams-voiceid-prompt button {
|
|
278
|
+
font: inherit;
|
|
279
|
+
cursor: pointer;
|
|
280
|
+
touch-action: manipulation;
|
|
281
|
+
}
|
|
282
|
+
.seams-voiceid-prompt button:focus-visible {
|
|
283
|
+
outline: 2px solid currentColor;
|
|
284
|
+
outline-offset: 2px;
|
|
285
|
+
}
|
|
286
|
+
.seams-voiceid-prompt .seams-voiceid-close {
|
|
287
|
+
position: absolute;
|
|
288
|
+
right: 0.75rem;
|
|
289
|
+
top: 0.75rem;
|
|
290
|
+
width: 2.5rem;
|
|
291
|
+
height: 2.5rem;
|
|
292
|
+
background: transparent;
|
|
293
|
+
border: 0;
|
|
294
|
+
border-radius: var(--seams-shape-control, 10px);
|
|
295
|
+
color: inherit;
|
|
296
|
+
display: grid;
|
|
297
|
+
place-items: center;
|
|
298
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
299
|
+
}
|
|
300
|
+
.seams-voiceid-prompt .seams-voiceid-close.seams-voiceid-back {
|
|
301
|
+
right: auto;
|
|
302
|
+
left: 0.75rem;
|
|
303
|
+
}
|
|
304
|
+
.seams-voiceid-prompt .seams-voiceid-record-slot {
|
|
305
|
+
grid-row: 5;
|
|
306
|
+
grid-column: 1;
|
|
307
|
+
align-self: center;
|
|
308
|
+
width: 100%;
|
|
309
|
+
box-sizing: border-box;
|
|
310
|
+
}
|
|
311
|
+
.seams-voiceid-prompt .seams-voiceid-primary {
|
|
312
|
+
width: 100%;
|
|
313
|
+
min-height: var(--seams-shape-controlHeight, 44px);
|
|
314
|
+
padding: 0.75rem 1rem;
|
|
315
|
+
font-size: 0.875rem;
|
|
316
|
+
font-weight: 500;
|
|
317
|
+
border: 0;
|
|
318
|
+
border-radius: var(--seams-shape-control, 10px);
|
|
319
|
+
background: var(--seams-colors-textPrimary, #eff2f3);
|
|
320
|
+
color: var(--seams-colors-colorBackground, #192024);
|
|
321
|
+
}
|
|
322
|
+
.seams-voiceid-prompt .seams-voiceid-primary {
|
|
323
|
+
transition: opacity 180ms ease, visibility 0s linear 0s;
|
|
324
|
+
}
|
|
325
|
+
.seams-voiceid-prompt .seams-voiceid-primary:disabled {
|
|
326
|
+
opacity: 0.5;
|
|
327
|
+
cursor: wait;
|
|
328
|
+
}
|
|
329
|
+
.seams-voiceid-prompt .seams-voiceid-record-slot { position: relative; }
|
|
330
|
+
/* The link sits under the button and never moves it. */
|
|
331
|
+
.seams-voiceid-prompt .seams-voiceid-microphone {
|
|
332
|
+
position: absolute;
|
|
333
|
+
top: 100%;
|
|
334
|
+
left: 50%;
|
|
335
|
+
translate: -50% 0;
|
|
336
|
+
white-space: nowrap;
|
|
337
|
+
transition: opacity 150ms ease, visibility 0s linear 0s;
|
|
338
|
+
margin-top: 0.5rem;
|
|
339
|
+
padding: 0.25rem;
|
|
340
|
+
border: 0;
|
|
341
|
+
background: transparent;
|
|
342
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
343
|
+
font-size: 0.75rem;
|
|
344
|
+
}
|
|
345
|
+
.seams-voiceid-prompt button[hidden],
|
|
346
|
+
.seams-voiceid-prompt [hidden] {
|
|
347
|
+
display: none !important;
|
|
348
|
+
}
|
|
349
|
+
/* The record button and the microphone link fade out in place and keep the row's layout. */
|
|
350
|
+
.seams-voiceid-activation-surface:not(.seams-voiceid-in-card) .seams-voiceid-record-slot > button[hidden] {
|
|
351
|
+
display: block !important;
|
|
352
|
+
opacity: 0;
|
|
353
|
+
visibility: hidden;
|
|
354
|
+
pointer-events: none;
|
|
355
|
+
transition: opacity 150ms ease, visibility 0s linear 150ms;
|
|
356
|
+
}
|
|
357
|
+
.seams-voiceid-activation-surface:not(.seams-voiceid-in-card) .seams-voiceid-record-slot > .seams-voiceid-primary[hidden] {
|
|
358
|
+
margin-inline: auto;
|
|
359
|
+
}
|
|
360
|
+
/* While VoiceID loads, its bar takes the answer line's place, its message the status line's,
|
|
361
|
+
and its retry the record button's. */
|
|
362
|
+
.seams-voiceid-progress-slot:has(> .seams-voiceid-model-download),
|
|
363
|
+
.seams-voiceid-progress-slot > .seams-voiceid-model-download { display: contents; }
|
|
364
|
+
.seams-voiceid-model-download > * { position: relative; z-index: 1; grid-column: 1; align-self: center; }
|
|
365
|
+
.seams-voiceid-prompt > .seams-voiceid-status { transition: opacity 180ms ease; }
|
|
366
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download:not([data-state="ready"])) > .seams-voiceid-ribbon,
|
|
367
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download:not([data-state="ready"])) > .seams-voiceid-status {
|
|
368
|
+
opacity: 0;
|
|
369
|
+
visibility: hidden;
|
|
370
|
+
transition: opacity 180ms ease, visibility 0s linear 180ms;
|
|
371
|
+
}
|
|
372
|
+
/* The download's rows fade in over the rows they replace, and fade out when VoiceID is ready. */
|
|
373
|
+
.seams-voiceid-model-download > * { transition: opacity 180ms ease 150ms; }
|
|
374
|
+
@starting-style { .seams-voiceid-model-download > * { opacity: 0; } }
|
|
375
|
+
/* Two texts in one place must not cross: the download's rows leave first, then the card's return. */
|
|
376
|
+
.seams-voiceid-model-download[data-state="ready"] > * { opacity: 0; transition-duration: 120ms; transition-delay: 0s; }
|
|
377
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download[data-state="ready"]) > .seams-voiceid-ribbon {
|
|
378
|
+
transition: opacity 180ms ease 120ms;
|
|
379
|
+
}
|
|
380
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download[data-state="ready"]) > .seams-voiceid-status {
|
|
381
|
+
transition: opacity 180ms ease 160ms;
|
|
382
|
+
}
|
|
383
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download:not([data-state="ready"])) > .seams-voiceid-phrase { opacity: 0.35; }
|
|
384
|
+
.seams-voiceid-prompt:has(.seams-voiceid-model-download[data-state="failed"]) > .seams-voiceid-record-slot { display: none; }
|
|
385
|
+
.seams-voiceid-model-download progress {
|
|
386
|
+
grid-row: 3;
|
|
387
|
+
justify-self: center;
|
|
388
|
+
appearance: none;
|
|
389
|
+
display: block;
|
|
390
|
+
width: min(100%, 308px);
|
|
391
|
+
height: 6px;
|
|
392
|
+
border: 0;
|
|
393
|
+
border-radius: 999px;
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
background: var(--seams-colors-surface, #202b31);
|
|
396
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
397
|
+
}
|
|
398
|
+
.seams-voiceid-model-download progress::-webkit-progress-bar { background: var(--seams-colors-surface, #202b31); }
|
|
399
|
+
.seams-voiceid-model-download progress::-webkit-progress-value { background: var(--seams-colors-textPrimary, #eff2f3); border-radius: 999px; }
|
|
400
|
+
/* The bar stays full and still while the models start: the start blocks the main thread, and
|
|
401
|
+
a looping animation would compete with it on a software-rendered page. */
|
|
402
|
+
.seams-voiceid-model-download progress::-moz-progress-bar { background: var(--seams-colors-textPrimary, #eff2f3); border-radius: 999px; }
|
|
403
|
+
.seams-voiceid-model-download p {
|
|
404
|
+
grid-row: 4;
|
|
405
|
+
align-self: start;
|
|
406
|
+
margin: 0;
|
|
407
|
+
font-size: 0.75rem;
|
|
408
|
+
line-height: 1.5;
|
|
409
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
410
|
+
font-variant-numeric: tabular-nums;
|
|
411
|
+
}
|
|
412
|
+
.seams-voiceid-model-download button { grid-row: 5; }
|
|
413
|
+
/* A phrase card shows its check and its result in the same card. */
|
|
414
|
+
.seams-voiceid-prompt > .seams-voiceid-status-line { grid-area: 3 / 1; }
|
|
415
|
+
.seams-voiceid-status-line {
|
|
416
|
+
transition: opacity 120ms ease;
|
|
417
|
+
display: block;
|
|
418
|
+
width: 308px;
|
|
419
|
+
max-width: 100%;
|
|
420
|
+
height: 64px;
|
|
421
|
+
margin: auto;
|
|
422
|
+
overflow: visible;
|
|
423
|
+
opacity: 0;
|
|
424
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
425
|
+
}
|
|
426
|
+
.seams-voiceid-ribbon { transition: opacity 120ms ease; }
|
|
427
|
+
.seams-voiceid-prompt[data-phrase-line="status"] > .seams-voiceid-ribbon { opacity: 0; }
|
|
428
|
+
.seams-voiceid-prompt[data-phrase-line="status"] > .seams-voiceid-status-line { opacity: 1; }
|
|
429
|
+
.seams-voiceid-status-line rect {
|
|
430
|
+
fill: var(--seams-colors-surface3, #2b363c);
|
|
431
|
+
transition: fill 240ms ease, opacity 240ms ease;
|
|
432
|
+
}
|
|
433
|
+
.seams-voiceid-status-line rect[data-state="done"],
|
|
434
|
+
.seams-voiceid-status-line rect[data-state="run"] { fill: currentColor; }
|
|
435
|
+
.seams-voiceid-status-line rect[data-state="run"] { animation: seams-voiceid-segment-pulse 900ms ease-in-out infinite alternate; }
|
|
436
|
+
.seams-voiceid-status-line rect[data-state="fail"] { fill: var(--seams-colors-error, oklch(0.74 0.166 19)); }
|
|
437
|
+
.seams-voiceid-status-line rect[data-state="wait"] {
|
|
438
|
+
fill: var(--seams-colors-info, oklch(0.7 0.13 255));
|
|
439
|
+
animation: seams-voiceid-segment-pulse 1600ms ease-in-out infinite alternate;
|
|
440
|
+
}
|
|
441
|
+
.seams-voiceid-status-line rect[data-state="skip"] { opacity: 0.5; }
|
|
442
|
+
@keyframes seams-voiceid-segment-pulse { from { opacity: 0.35; } to { opacity: 1; } }
|
|
443
|
+
.seams-voiceid-status-bar,
|
|
444
|
+
.seams-voiceid-status-ring,
|
|
445
|
+
.seams-voiceid-status-seal,
|
|
446
|
+
.seams-voiceid-status-check { opacity: 0; transform-box: fill-box; transform-origin: center; }
|
|
447
|
+
.seams-voiceid-status-bar { fill: none; stroke: currentColor; stroke-width: 3; stroke-linecap: round; }
|
|
448
|
+
.seams-voiceid-status-ring { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; }
|
|
449
|
+
.seams-voiceid-status-seal { fill: currentColor; transform: scale(0); }
|
|
450
|
+
.seams-voiceid-status-check {
|
|
451
|
+
fill: none;
|
|
452
|
+
stroke: var(--seams-colors-colorBackground, #192024);
|
|
453
|
+
stroke-width: 2.6;
|
|
454
|
+
stroke-linecap: round;
|
|
455
|
+
stroke-linejoin: round;
|
|
456
|
+
stroke-dasharray: 1;
|
|
457
|
+
stroke-dashoffset: 1;
|
|
458
|
+
}
|
|
459
|
+
.seams-voiceid-status-line[data-seal] rect { opacity: 0; }
|
|
460
|
+
.seams-voiceid-status-line[data-seal] > :not(rect) { opacity: 1; }
|
|
461
|
+
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-phrase-dim { color: var(--seams-colors-textMuted, #6f7f87); }
|
|
462
|
+
.seams-voiceid-prompt[data-phrase-view] > h2 { min-height: 1.19rem; }
|
|
463
|
+
.seams-voiceid-prompt[data-phrase-view="check"] {
|
|
464
|
+
grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
|
|
465
|
+
}
|
|
466
|
+
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-record-slot { display: none; }
|
|
467
|
+
.seams-voiceid-prompt[data-phrase-view] .seams-voiceid-microphone { display: none; }
|
|
468
|
+
.seams-voiceid-prompt .seams-voiceid-close { transition: opacity 150ms ease, visibility 0s linear 0s; }
|
|
469
|
+
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-close {
|
|
470
|
+
opacity: 0;
|
|
471
|
+
visibility: hidden;
|
|
472
|
+
transition: opacity 150ms ease, visibility 0s linear 150ms;
|
|
473
|
+
}
|
|
474
|
+
.seams-voiceid-status.seams-voiceid-two-lines > span { display: block; }
|
|
475
|
+
.seams-voiceid-status .seams-voiceid-caption {
|
|
476
|
+
margin-bottom: 0.125rem;
|
|
477
|
+
font-size: 0.8125rem;
|
|
478
|
+
font-weight: 500;
|
|
479
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
480
|
+
}
|
|
481
|
+
.seams-voiceid-status a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
|
482
|
+
.seams-voiceid-heard-mark {
|
|
483
|
+
background: none;
|
|
484
|
+
color: var(--seams-colors-error, oklch(0.74 0.166 19));
|
|
485
|
+
text-decoration: underline;
|
|
486
|
+
text-decoration-thickness: 1.5px;
|
|
487
|
+
text-underline-offset: 3px;
|
|
488
|
+
}
|
|
489
|
+
.seams-voiceid-prompt .seams-voiceid-primary:has(> .seams-voiceid-action-icon) {
|
|
490
|
+
display: flex;
|
|
491
|
+
align-items: center;
|
|
492
|
+
justify-content: center;
|
|
493
|
+
gap: 0.5rem;
|
|
494
|
+
}
|
|
495
|
+
.seams-voiceid-action-icon {
|
|
496
|
+
fill: none;
|
|
497
|
+
stroke: currentColor;
|
|
498
|
+
stroke-width: 1.75;
|
|
499
|
+
stroke-linecap: round;
|
|
500
|
+
stroke-linejoin: round;
|
|
501
|
+
}
|
|
502
|
+
.seams-voiceid-prompt[data-phrase-view] .seams-voiceid-primary:disabled { cursor: default; }
|
|
503
|
+
@media (prefers-reduced-motion: reduce) {
|
|
504
|
+
.seams-voiceid-status-line rect { animation: none !important; transition: none; }
|
|
505
|
+
}
|
|
506
|
+
@media (hover: hover) and (pointer: fine) {
|
|
507
|
+
.seams-voiceid-close:hover { background: var(--seams-colors-surface, #202b31); }
|
|
508
|
+
.seams-voiceid-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
509
|
+
}
|
|
510
|
+
`;
|
|
511
|
+
/** The documents that have the prompt's styles. */
|
|
512
|
+
const styled = /* @__PURE__ */ new WeakSet();
|
|
513
|
+
/**
|
|
514
|
+
* Adds the prompt's styles to the document one time: as a constructed stylesheet where the
|
|
515
|
+
* browser has them, so a strict style policy needs no nonce, else as a style element with the
|
|
516
|
+
* page's nonce.
|
|
517
|
+
*/
|
|
518
|
+
function ensureStyles(document) {
|
|
519
|
+
if (styled.has(document)) return;
|
|
520
|
+
styled.add(document);
|
|
521
|
+
try {
|
|
522
|
+
const sheet = new CSSStyleSheet();
|
|
523
|
+
sheet.replaceSync(CSS);
|
|
524
|
+
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
|
|
525
|
+
return;
|
|
526
|
+
} catch {}
|
|
527
|
+
const style = document.createElement("style");
|
|
528
|
+
const nonce = styleNonce();
|
|
529
|
+
if (nonce) style.setAttribute("nonce", nonce);
|
|
530
|
+
style.textContent = CSS;
|
|
531
|
+
(document.head ?? document.documentElement).appendChild(style);
|
|
532
|
+
}
|
|
533
|
+
var VoicePrompt = class {
|
|
534
|
+
element;
|
|
535
|
+
title;
|
|
536
|
+
phrase;
|
|
537
|
+
status;
|
|
538
|
+
record;
|
|
539
|
+
cancel;
|
|
540
|
+
progress;
|
|
541
|
+
/** The wave under the phrase. */
|
|
542
|
+
ribbon;
|
|
543
|
+
/** The row of the record button, which a phrase card reuses for its action. */
|
|
544
|
+
actionRow;
|
|
545
|
+
microphone;
|
|
546
|
+
microphoneRequested = false;
|
|
547
|
+
visual;
|
|
548
|
+
disposed = false;
|
|
549
|
+
pendingChange = null;
|
|
550
|
+
changing = null;
|
|
551
|
+
/** The card has been laid out once, and has since been hidden (a Wallet confirm took its place). */
|
|
552
|
+
laidOut = false;
|
|
553
|
+
hidden = false;
|
|
554
|
+
sizes = new ResizeObserver(this.observeSize.bind(this));
|
|
555
|
+
/** A newer resize stops the one in progress, and takes over what waits for it to end. */
|
|
556
|
+
frameFill = 0;
|
|
557
|
+
onFrameFitted = [];
|
|
558
|
+
constructor(document, label, phrase, checking = "ring", display = (spoken) => spoken) {
|
|
559
|
+
this.display = display;
|
|
560
|
+
ensureStyles(document);
|
|
561
|
+
this.element = document.createElement("section");
|
|
562
|
+
this.element.className = "seams-voiceid-prompt";
|
|
563
|
+
this.element.setAttribute("aria-label", label);
|
|
564
|
+
this.title = document.createElement("h2");
|
|
565
|
+
this.title.textContent = "Read aloud";
|
|
566
|
+
this.phrase = document.createElement("p");
|
|
567
|
+
this.phrase.className = "seams-voiceid-phrase";
|
|
568
|
+
this.phrase.dataset.voiceidPhrase = "";
|
|
569
|
+
this.phrase.setAttribute("aria-live", "polite");
|
|
570
|
+
this.setPhrase(phrase);
|
|
571
|
+
this.visual = new VoiceVisual(document, this.element, checking);
|
|
572
|
+
this.status = document.createElement("p");
|
|
573
|
+
this.status.className = "seams-voiceid-status";
|
|
574
|
+
this.status.setAttribute("role", "status");
|
|
575
|
+
this.record = document.createElement("button");
|
|
576
|
+
this.record.type = "button";
|
|
577
|
+
this.record.className = "seams-voiceid-primary";
|
|
578
|
+
this.record.textContent = "Record";
|
|
579
|
+
this.cancel = document.createElement("button");
|
|
580
|
+
this.cancel.type = "button";
|
|
581
|
+
this.cancel.className = "seams-voiceid-close";
|
|
582
|
+
const closeIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
583
|
+
closeIcon.setAttribute("viewBox", "0 0 24 24");
|
|
584
|
+
closeIcon.setAttribute("width", "20");
|
|
585
|
+
closeIcon.setAttribute("height", "20");
|
|
586
|
+
closeIcon.setAttribute("aria-hidden", "true");
|
|
587
|
+
const closePath = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
588
|
+
closePath.setAttribute("d", "M6 6l12 12M18 6L6 18");
|
|
589
|
+
closePath.setAttribute("fill", "none");
|
|
590
|
+
closePath.setAttribute("stroke", "currentColor");
|
|
591
|
+
closePath.setAttribute("stroke-width", "1.75");
|
|
592
|
+
closePath.setAttribute("stroke-linecap", "round");
|
|
593
|
+
closeIcon.append(closePath);
|
|
594
|
+
this.cancel.append(closeIcon);
|
|
595
|
+
this.cancel.setAttribute("aria-label", "Cancel");
|
|
596
|
+
this.progress = document.createElement("div");
|
|
597
|
+
this.progress.className = "seams-voiceid-progress-slot";
|
|
598
|
+
const slot = document.createElement("div");
|
|
599
|
+
slot.className = "seams-voiceid-record-slot";
|
|
600
|
+
this.actionRow = slot;
|
|
601
|
+
this.ribbon = this.visual.element;
|
|
602
|
+
this.microphone = document.createElement("button");
|
|
603
|
+
this.microphone.type = "button";
|
|
604
|
+
this.microphone.className = "seams-voiceid-microphone";
|
|
605
|
+
this.microphone.textContent = "Use system microphone";
|
|
606
|
+
this.microphone.hidden = true;
|
|
607
|
+
this.microphone.addEventListener("click", this.changeMicrophone);
|
|
608
|
+
slot.append(this.record, this.microphone);
|
|
609
|
+
this.element.append(this.visual.glow, this.cancel, this.title, this.phrase, this.visual.element, this.status, slot, this.progress);
|
|
610
|
+
this.sizes.observe(this.element);
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* In an app's dialog with a step before this card, the corner control returns to that step.
|
|
614
|
+
* It still ends the card as cancelled; the Wallet tells the app that the person went back.
|
|
615
|
+
*/
|
|
616
|
+
showBack() {
|
|
617
|
+
this.cancel.classList.add("seams-voiceid-back");
|
|
618
|
+
this.cancel.setAttribute("aria-label", "Back");
|
|
619
|
+
this.cancel.querySelector("path")?.setAttribute("d", "m15 18-6-6 6-6");
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Moves the rows to a new state. The rows whose content changes fade out together, take their
|
|
623
|
+
* new content and fade in one after another; the other rows stay still. A change that comes
|
|
624
|
+
* during a fade joins it, so no state in between is drawn.
|
|
625
|
+
*/
|
|
626
|
+
change(next) {
|
|
627
|
+
this.pendingChange = mergeChange(this.pendingChange, next);
|
|
628
|
+
this.changing ??= this.drainChanges().finally(this.changesDrained);
|
|
629
|
+
return this.changing;
|
|
630
|
+
}
|
|
631
|
+
changesDrained = () => {
|
|
632
|
+
this.changing = null;
|
|
633
|
+
};
|
|
634
|
+
async drainChanges() {
|
|
635
|
+
let faded = [];
|
|
636
|
+
while (this.pendingChange && !this.disposed) {
|
|
637
|
+
const next = this.pendingChange;
|
|
638
|
+
this.pendingChange = null;
|
|
639
|
+
const leaving = this.changingRows(next).filter((row) => !faded.includes(row));
|
|
640
|
+
if (leaving.length > 0 && !reducedMotion()) {
|
|
641
|
+
await Promise.all(leaving.map(fadeRowOut));
|
|
642
|
+
faded = this.rowOrder([...faded, ...leaving]);
|
|
643
|
+
if (this.pendingChange) {
|
|
644
|
+
this.pendingChange = mergeChange(next, this.pendingChange);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
} else faded = this.rowOrder([...faded, ...leaving]);
|
|
648
|
+
if (this.disposed) return;
|
|
649
|
+
this.writeChange(next);
|
|
650
|
+
fadeRowsIn(faded);
|
|
651
|
+
faded = [];
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** The rows whose visible content `next` changes, top to bottom. */
|
|
655
|
+
changingRows(next) {
|
|
656
|
+
const rows = [];
|
|
657
|
+
if (next.title !== void 0 && next.title !== this.title.textContent) rows.push(this.title);
|
|
658
|
+
if (next.phrase !== void 0 && next.phrase !== this.phrase.dataset.voiceidPhrase) rows.push(this.phrase);
|
|
659
|
+
if (next.status !== void 0 && next.status !== this.status.textContent) rows.push(this.status);
|
|
660
|
+
const action = next.action;
|
|
661
|
+
if (action && (action.label !== void 0 && action.label !== this.record.textContent || action.hidden !== void 0 && action.hidden !== this.record.hidden)) rows.push(this.actionRow);
|
|
662
|
+
return rows;
|
|
663
|
+
}
|
|
664
|
+
rowOrder(rows) {
|
|
665
|
+
return [
|
|
666
|
+
this.title,
|
|
667
|
+
this.phrase,
|
|
668
|
+
this.status,
|
|
669
|
+
this.actionRow
|
|
670
|
+
].filter((row) => rows.includes(row));
|
|
671
|
+
}
|
|
672
|
+
writeChange(next) {
|
|
673
|
+
if (next.title !== void 0) this.title.textContent = next.title;
|
|
674
|
+
if (next.phrase !== void 0) this.setPhrase(next.phrase);
|
|
675
|
+
if (next.status !== void 0) this.status.textContent = next.status;
|
|
676
|
+
if (next.action?.label !== void 0) this.record.textContent = next.action.label;
|
|
677
|
+
if (next.action?.hidden !== void 0) this.record.hidden = next.action.hidden;
|
|
678
|
+
next.apply?.();
|
|
679
|
+
}
|
|
680
|
+
/** The card is ready to record again, through a change of its rows. */
|
|
681
|
+
readyChange(message, next = {}) {
|
|
682
|
+
return this.change({
|
|
683
|
+
...next,
|
|
684
|
+
status: message,
|
|
685
|
+
action: {
|
|
686
|
+
...next.action,
|
|
687
|
+
hidden: false
|
|
688
|
+
},
|
|
689
|
+
apply: () => {
|
|
690
|
+
next.apply?.();
|
|
691
|
+
this.unlockLayout();
|
|
692
|
+
this.visual.ready();
|
|
693
|
+
this.element.dataset.capture = "ready";
|
|
694
|
+
this.record.disabled = false;
|
|
695
|
+
this.microphone.hidden = !this.microphoneRequested;
|
|
696
|
+
}
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
/** Starts without an entrance, because it takes another card's place. */
|
|
700
|
+
skipEntrance() {
|
|
701
|
+
this.element.classList.add("seams-voiceid-entered");
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* In the Wallet iframe the parent eases the frame to every new card size. While the frame is
|
|
705
|
+
* taller than the card, the card fills it, so its edge follows the frame and never jumps; the
|
|
706
|
+
* surface around it keeps the card's own height, which is what the parent hears.
|
|
707
|
+
*/
|
|
708
|
+
fillFrame(fromCssPx = window.innerHeight, rows = "top", onFitted) {
|
|
709
|
+
if (onFitted) this.onFrameFitted.push(onFitted);
|
|
710
|
+
const surface = this.element.closest(".seams-voiceid-hugged");
|
|
711
|
+
if (!surface || this.disposed) {
|
|
712
|
+
this.frameFitted();
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const fill = ++this.frameFill;
|
|
716
|
+
const centred = rows === "center" || this.element.dataset.frameFill === "center";
|
|
717
|
+
this.element.style.minHeight = "";
|
|
718
|
+
surface.style.height = "";
|
|
719
|
+
delete this.element.dataset.frameFill;
|
|
720
|
+
const own = this.element.getBoundingClientRect().height;
|
|
721
|
+
if (fromCssPx <= own + 1) {
|
|
722
|
+
this.frameFitted();
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
surface.style.height = `${own}px`;
|
|
726
|
+
if (centred) this.element.dataset.frameFill = "center";
|
|
727
|
+
let height = fromCssPx;
|
|
728
|
+
this.element.style.minHeight = `${height}px`;
|
|
729
|
+
let fitted = 0;
|
|
730
|
+
const began = performance.now();
|
|
731
|
+
const step = () => {
|
|
732
|
+
if (fill !== this.frameFill || this.disposed) return;
|
|
733
|
+
height = Math.min(height, Math.max(window.innerHeight, own));
|
|
734
|
+
this.element.style.minHeight = `${height}px`;
|
|
735
|
+
fitted = window.innerHeight <= own + 1 ? fitted + 1 : 0;
|
|
736
|
+
if (fitted < 2 && performance.now() - began < FRAME_FILL_LIMIT_MS) {
|
|
737
|
+
requestAnimationFrame(step);
|
|
738
|
+
return;
|
|
739
|
+
}
|
|
740
|
+
this.element.style.minHeight = "";
|
|
741
|
+
surface.style.height = "";
|
|
742
|
+
delete this.element.dataset.frameFill;
|
|
743
|
+
this.frameFitted();
|
|
744
|
+
};
|
|
745
|
+
requestAnimationFrame(step);
|
|
746
|
+
}
|
|
747
|
+
releaseRows = () => {
|
|
748
|
+
delete this.element.dataset.rowsHeld;
|
|
749
|
+
fadeRowsIn(this.rows());
|
|
750
|
+
};
|
|
751
|
+
frameFitted() {
|
|
752
|
+
for (const fitted of this.onFrameFitted.splice(0)) fitted();
|
|
753
|
+
}
|
|
754
|
+
/** The card's rows, top to bottom: everything but its glow, its close button and its download. */
|
|
755
|
+
rows() {
|
|
756
|
+
return [...this.element.children].filter((row) => !row.matches(".seams-voiceid-glow, .seams-voiceid-close, .seams-voiceid-progress-slot"));
|
|
757
|
+
}
|
|
758
|
+
/** A Wallet confirm hid the card (wallet-service.css); when it shows again, its rows come back. */
|
|
759
|
+
observeSize(entries) {
|
|
760
|
+
const box = entries.at(-1)?.contentRect;
|
|
761
|
+
if (!box || this.disposed) return;
|
|
762
|
+
if (box.height === 0) {
|
|
763
|
+
if (!this.laidOut) return;
|
|
764
|
+
this.hidden = true;
|
|
765
|
+
this.skipEntrance();
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
if (!this.laidOut) {
|
|
769
|
+
this.laidOut = true;
|
|
770
|
+
if (!this.element.classList.contains("seams-voiceid-entered")) fadeRowsIn(this.rows());
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (!this.hidden) return;
|
|
774
|
+
this.hidden = false;
|
|
775
|
+
this.element.dataset.rowsHeld = "";
|
|
776
|
+
this.fillFrame(window.innerHeight, "center", this.releaseRows);
|
|
777
|
+
const focused = this.element.ownerDocument.activeElement;
|
|
778
|
+
if (!focused || focused === this.element.ownerDocument.body) {
|
|
779
|
+
this.element.tabIndex = -1;
|
|
780
|
+
this.element.focus({ preventScroll: true });
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
setPhrase(phrase) {
|
|
784
|
+
const displayPhrase = this.display(phrase);
|
|
785
|
+
this.phrase.textContent = displayPhrase.charAt(0).toUpperCase() + displayPhrase.slice(1) + ".";
|
|
786
|
+
this.phrase.dataset.voiceidPhrase = phrase;
|
|
787
|
+
}
|
|
788
|
+
capture = (event) => {
|
|
789
|
+
if (this.disposed) return;
|
|
790
|
+
switch (event.kind) {
|
|
791
|
+
case "opening":
|
|
792
|
+
this.microphoneRequested = true;
|
|
793
|
+
this.microphone.hidden = true;
|
|
794
|
+
return;
|
|
795
|
+
case "listening":
|
|
796
|
+
this.microphone.hidden = true;
|
|
797
|
+
this.element.dataset.capture = "listening";
|
|
798
|
+
this.change({
|
|
799
|
+
status: "Listening",
|
|
800
|
+
action: { hidden: true }
|
|
801
|
+
});
|
|
802
|
+
this.visual.listen();
|
|
803
|
+
return;
|
|
804
|
+
case "activity":
|
|
805
|
+
this.visual.add(event);
|
|
806
|
+
return;
|
|
807
|
+
case "measured":
|
|
808
|
+
if (event.quality.noise === "high") this.status.textContent = "Background noise is high. Move closer to the microphone.";
|
|
809
|
+
return;
|
|
810
|
+
case "processing":
|
|
811
|
+
this.element.dataset.capture = "processing";
|
|
812
|
+
this.lockLayout();
|
|
813
|
+
this.visual.checking();
|
|
814
|
+
this.change({ status: "Checking your voice…" });
|
|
815
|
+
return;
|
|
816
|
+
default: return assertNever(event);
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
/**
|
|
820
|
+
* This card replaces `previous` in the same surface. It skips the entrance and starts with the
|
|
821
|
+
* rows that `previous` shows, so the swap does not show; the caller then changes the rows.
|
|
822
|
+
*/
|
|
823
|
+
continueFrom(previous) {
|
|
824
|
+
this.skipEntrance();
|
|
825
|
+
this.element.tabIndex = -1;
|
|
826
|
+
const title = previous.querySelector(":scope > h2")?.textContent;
|
|
827
|
+
const status = previous.querySelector(":scope > .seams-voiceid-status")?.textContent;
|
|
828
|
+
const button = previous.querySelector(":scope > .seams-voiceid-record-slot > .seams-voiceid-primary");
|
|
829
|
+
if (title !== void 0 && title !== null) this.title.textContent = title;
|
|
830
|
+
if (status !== void 0 && status !== null) this.status.textContent = status;
|
|
831
|
+
if (button) {
|
|
832
|
+
this.record.textContent = button.textContent;
|
|
833
|
+
this.record.hidden = button.hidden;
|
|
834
|
+
}
|
|
835
|
+
const fading = [
|
|
836
|
+
[previous.querySelector(":scope > h2"), this.title],
|
|
837
|
+
[previous.querySelector(":scope > .seams-voiceid-phrase"), this.phrase],
|
|
838
|
+
[previous.querySelector(":scope > .seams-voiceid-status"), this.status],
|
|
839
|
+
[previous.querySelector(":scope > .seams-voiceid-record-slot"), this.actionRow]
|
|
840
|
+
].flatMap(([old, row]) => {
|
|
841
|
+
const opacity = old ? Number(getComputedStyle(old).opacity) : 1;
|
|
842
|
+
return opacity < .99 ? [{
|
|
843
|
+
row,
|
|
844
|
+
opacity
|
|
845
|
+
}] : [];
|
|
846
|
+
});
|
|
847
|
+
if (fading.length > 0) queueMicrotask(() => fading.forEach(continueFade));
|
|
848
|
+
}
|
|
849
|
+
/** Settle the visual when an enrollment sample is accepted. */
|
|
850
|
+
accepted() {
|
|
851
|
+
this.visual.accept();
|
|
852
|
+
}
|
|
853
|
+
/** The wave rests flat while the glow keeps working. */
|
|
854
|
+
flattenLine() {
|
|
855
|
+
this.visual.flatten();
|
|
856
|
+
}
|
|
857
|
+
/** The glow settles. */
|
|
858
|
+
quietLine() {
|
|
859
|
+
this.visual.ready();
|
|
860
|
+
}
|
|
861
|
+
/**
|
|
862
|
+
* Plays the unlock success in place and resolves when the check is drawn. Once
|
|
863
|
+
* the ring has formed it glides to the middle of `area` (the card by default).
|
|
864
|
+
*/
|
|
865
|
+
async succeed(headline, message, area = this.element.getBoundingClientRect()) {
|
|
866
|
+
if (this.disposed) return;
|
|
867
|
+
this.element.dataset.capture = "succeeded";
|
|
868
|
+
this.record.hidden = true;
|
|
869
|
+
this.microphone.hidden = true;
|
|
870
|
+
await this.visual.succeed(this.beginSuccess.bind(this, headline, message, area));
|
|
871
|
+
}
|
|
872
|
+
ready(message) {
|
|
873
|
+
this.unlockLayout();
|
|
874
|
+
this.visual.ready();
|
|
875
|
+
this.element.dataset.capture = "ready";
|
|
876
|
+
this.status.textContent = message;
|
|
877
|
+
this.record.hidden = false;
|
|
878
|
+
this.record.disabled = false;
|
|
879
|
+
this.microphone.hidden = !this.microphoneRequested;
|
|
880
|
+
}
|
|
881
|
+
changeMicrophone = () => {
|
|
882
|
+
this.useSystemMicrophone();
|
|
883
|
+
};
|
|
884
|
+
async useSystemMicrophone() {
|
|
885
|
+
this.record.disabled = true;
|
|
886
|
+
this.microphone.disabled = true;
|
|
887
|
+
await useSystemVoiceMicrophone();
|
|
888
|
+
if (this.disposed) return;
|
|
889
|
+
this.status.textContent = "The next recording will use your system microphone.";
|
|
890
|
+
this.record.disabled = false;
|
|
891
|
+
this.microphone.disabled = false;
|
|
892
|
+
this.microphone.hidden = true;
|
|
893
|
+
this.record.focus();
|
|
894
|
+
}
|
|
895
|
+
dispose() {
|
|
896
|
+
this.disposed = true;
|
|
897
|
+
this.sizes.disconnect();
|
|
898
|
+
this.microphone.removeEventListener("click", this.changeMicrophone);
|
|
899
|
+
this.visual.stop();
|
|
900
|
+
}
|
|
901
|
+
/** The ribbon turns into the ring and the copy may change, so the card's rows hold their size. */
|
|
902
|
+
lockLayout() {
|
|
903
|
+
for (const part of [
|
|
904
|
+
this.element,
|
|
905
|
+
this.phrase,
|
|
906
|
+
this.status
|
|
907
|
+
]) part.style.minHeight = `${part.getBoundingClientRect().height}px`;
|
|
908
|
+
this.element.style.height = this.element.style.minHeight;
|
|
909
|
+
}
|
|
910
|
+
/**
|
|
911
|
+
* The ring has formed: the reading copy fades where it is, only the ring glides
|
|
912
|
+
* to the middle of the free space, and the success copy arrives in place around
|
|
913
|
+
* it, headline first. The rows keep their locked size throughout.
|
|
914
|
+
*/
|
|
915
|
+
async beginSuccess(headline, message, area) {
|
|
916
|
+
if (this.disposed) return;
|
|
917
|
+
const ring = this.visual.element.getBoundingClientRect();
|
|
918
|
+
const lift = (area.top + area.bottom - ring.top - ring.bottom) / 2;
|
|
919
|
+
this.visual.element.animate([{ translate: "0 0" }, { translate: `0 ${lift}px` }], {
|
|
920
|
+
duration: 600,
|
|
921
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
922
|
+
fill: "forwards"
|
|
923
|
+
});
|
|
924
|
+
await Promise.all([
|
|
925
|
+
this.title,
|
|
926
|
+
this.phrase,
|
|
927
|
+
this.status
|
|
928
|
+
].map(fadeOut));
|
|
929
|
+
if (this.disposed) return;
|
|
930
|
+
this.phrase.textContent = headline;
|
|
931
|
+
this.phrase.classList.add("seams-voiceid-success-headline");
|
|
932
|
+
this.status.textContent = message;
|
|
933
|
+
this.status.classList.add("seams-voiceid-success-message");
|
|
934
|
+
const reduced = matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
935
|
+
for (const [index, part] of [this.phrase, this.status].entries()) {
|
|
936
|
+
part.style.translate = `0 ${lift}px`;
|
|
937
|
+
fadeIn(part, reduced, SUCCESS_COPY_DELAY_MS - FADE_OUT_MS + index * SUCCESS_COPY_STAGGER_MS);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
unlockLayout() {
|
|
941
|
+
for (const part of [
|
|
942
|
+
this.element,
|
|
943
|
+
this.phrase,
|
|
944
|
+
this.status
|
|
945
|
+
]) part.style.minHeight = "";
|
|
946
|
+
this.element.style.height = "";
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const FADE_OUT_MS = 150;
|
|
950
|
+
const SUCCESS_COPY_DELAY_MS = 450;
|
|
951
|
+
const SUCCESS_COPY_STAGGER_MS = 40;
|
|
952
|
+
function fadeOut(element) {
|
|
953
|
+
return element.animate([{ opacity: 1 }, { opacity: 0 }], {
|
|
954
|
+
duration: FADE_OUT_MS,
|
|
955
|
+
easing: "ease-in",
|
|
956
|
+
fill: "forwards"
|
|
957
|
+
}).finished;
|
|
958
|
+
}
|
|
959
|
+
function fadeIn(element, reduced, delay) {
|
|
960
|
+
element.animate([{
|
|
961
|
+
opacity: 0,
|
|
962
|
+
transform: reduced ? "none" : "translateY(4px)"
|
|
963
|
+
}, {
|
|
964
|
+
opacity: 1,
|
|
965
|
+
transform: "none"
|
|
966
|
+
}], {
|
|
967
|
+
delay,
|
|
968
|
+
duration: 300,
|
|
969
|
+
easing: "cubic-bezier(0.2, 0, 0, 1)",
|
|
970
|
+
fill: "both"
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
function assertNever(value) {
|
|
974
|
+
throw new Error(`Unexpected capture event: ${String(value)}`);
|
|
975
|
+
}
|
|
976
|
+
const ROW_OUT_MS = 150;
|
|
977
|
+
const ROW_IN_MS = 300;
|
|
978
|
+
const ROW_IN_DELAY_MS = 60;
|
|
979
|
+
const ROW_STAGGER_MS = 40;
|
|
980
|
+
const ROW_EASE = "cubic-bezier(0.2, 0, 0, 1)";
|
|
981
|
+
const ROW_MOTION = "seams-voiceid-row";
|
|
982
|
+
/** A frame that never reaches the card's size stops being followed after this time. */
|
|
983
|
+
const FRAME_FILL_LIMIT_MS = 800;
|
|
984
|
+
function reducedMotion() {
|
|
985
|
+
return matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
986
|
+
}
|
|
987
|
+
function mergeChange(previous, next) {
|
|
988
|
+
if (!previous) return next;
|
|
989
|
+
const applies = [previous.apply, next.apply].filter((apply) => apply !== void 0);
|
|
990
|
+
return {
|
|
991
|
+
...previous,
|
|
992
|
+
...next,
|
|
993
|
+
action: previous.action || next.action ? {
|
|
994
|
+
...previous.action,
|
|
995
|
+
...next.action
|
|
996
|
+
} : void 0,
|
|
997
|
+
apply: applies.length === 0 ? void 0 : () => applies.forEach(run)
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
function run(apply) {
|
|
1001
|
+
apply();
|
|
1002
|
+
}
|
|
1003
|
+
function cancelRowMotion(row) {
|
|
1004
|
+
for (const animation of row.getAnimations()) if (animation.id === ROW_MOTION) animation.cancel();
|
|
1005
|
+
}
|
|
1006
|
+
/** Fades a row out from where it is; it stays out until it fades in again. */
|
|
1007
|
+
function fadeRowOut(row) {
|
|
1008
|
+
const from = Number(getComputedStyle(row).opacity);
|
|
1009
|
+
cancelRowMotion(row);
|
|
1010
|
+
return row.animate([{ opacity: from }, { opacity: 0 }], {
|
|
1011
|
+
duration: ROW_OUT_MS * from,
|
|
1012
|
+
easing: "ease-in",
|
|
1013
|
+
fill: "forwards",
|
|
1014
|
+
id: ROW_MOTION
|
|
1015
|
+
}).finished.catch(ignoreCancel);
|
|
1016
|
+
}
|
|
1017
|
+
/** Rows arrive one after another, each to the opacity that the card's styles give it. */
|
|
1018
|
+
function fadeRowsIn(rows) {
|
|
1019
|
+
const reduced = reducedMotion();
|
|
1020
|
+
for (const [index, row] of rows.entries()) {
|
|
1021
|
+
cancelRowMotion(row);
|
|
1022
|
+
const to = Number(getComputedStyle(row).opacity);
|
|
1023
|
+
if (to === 0) continue;
|
|
1024
|
+
row.animate([{
|
|
1025
|
+
opacity: 0,
|
|
1026
|
+
transform: reduced ? "none" : "translateY(4px)"
|
|
1027
|
+
}, {
|
|
1028
|
+
opacity: to,
|
|
1029
|
+
transform: "none"
|
|
1030
|
+
}], {
|
|
1031
|
+
delay: reduced ? 0 : ROW_IN_DELAY_MS + index * ROW_STAGGER_MS,
|
|
1032
|
+
duration: reduced ? 150 : ROW_IN_MS,
|
|
1033
|
+
easing: ROW_EASE,
|
|
1034
|
+
fill: "backwards",
|
|
1035
|
+
id: ROW_MOTION
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
function ignoreCancel() {}
|
|
1040
|
+
function continueFade({ row, opacity }) {
|
|
1041
|
+
row.animate([{ opacity }, { opacity: 1 }], {
|
|
1042
|
+
duration: ROW_IN_MS * (1 - opacity),
|
|
1043
|
+
easing: ROW_EASE,
|
|
1044
|
+
id: ROW_MOTION
|
|
1045
|
+
});
|
|
1046
|
+
}
|
|
1047
|
+
/**
|
|
1048
|
+
* A closed card fades out where it is, then goes. It leaves the layout at once, so a card or a
|
|
1049
|
+
* confirm that comes next takes its place, and the frame never shows empty before it closes.
|
|
1050
|
+
*/
|
|
1051
|
+
function leaveSurface(element) {
|
|
1052
|
+
if (!element.isConnected || reducedMotion()) {
|
|
1053
|
+
element.remove();
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
element.inert = true;
|
|
1057
|
+
element.classList.remove("seams-auth-method-activation-surface");
|
|
1058
|
+
delete element.dataset.seamsVoiceidHandoff;
|
|
1059
|
+
const box = element.getBoundingClientRect();
|
|
1060
|
+
element.style.position = "fixed";
|
|
1061
|
+
element.style.top = `${box.top}px`;
|
|
1062
|
+
element.style.left = `${box.left}px`;
|
|
1063
|
+
element.style.width = `${box.width}px`;
|
|
1064
|
+
element.style.margin = "0";
|
|
1065
|
+
element.style.pointerEvents = "none";
|
|
1066
|
+
const from = Number(getComputedStyle(element).opacity);
|
|
1067
|
+
element.animate([{ opacity: from }, { opacity: 0 }], {
|
|
1068
|
+
duration: ROW_OUT_MS * from,
|
|
1069
|
+
easing: "ease-in",
|
|
1070
|
+
fill: "forwards"
|
|
1071
|
+
}).finished.catch(ignoreCancel).finally(() => element.remove());
|
|
1072
|
+
}
|
|
1073
|
+
//#endregion
|
|
1074
|
+
export { VoicePrompt, leaveSurface };
|