@seams/voice-capture 0.68.1 → 0.69.1
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/README.md +42 -1
- package/dist/browser/card.js +253 -19
- package/dist/esm/card/index.js +1 -1
- package/dist/esm/card/phraseCard.js +2 -2
- package/dist/esm/card/prompt.js +315 -20
- package/dist/esm/card/ribbon.js +92 -2
- package/dist/esm/card/rowMotion.js +3 -3
- package/dist/esm/card/surface.js +114 -5
- package/dist/esm/card/visual.js +1 -1
- package/dist/esm/chunks/{visual-CeEQ8QkM.js → visual-rOVJCP2w.js} +13 -2
- package/dist/types/card/index.d.ts +2 -2
- package/dist/types/card/prompt.d.ts +13 -1
- package/dist/types/card/ribbon.d.ts +17 -0
- package/dist/types/card/rowMotion.d.ts +3 -3
- package/dist/types/card/surface.d.ts +65 -0
- package/dist/types/card/visual.d.ts +4 -2
- package/package.json +1 -1
package/dist/esm/card/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PhraseCard } from "./phraseCard.js";
|
|
2
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
2
|
+
import { t as VoiceVisual } from "../chunks/visual-rOVJCP2w.js";
|
|
3
3
|
import { VoiceRibbon } from "./ribbon.js";
|
|
4
4
|
import { VoicePrompt, leaveSurface } from "./prompt.js";
|
|
5
5
|
import { openPhraseCard } from "./surface.js";
|
|
@@ -229,7 +229,7 @@ var PhraseCard = class {
|
|
|
229
229
|
duration: RIBBON_MOVE_MS,
|
|
230
230
|
easing: EASE_MOVE
|
|
231
231
|
});
|
|
232
|
-
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches,
|
|
232
|
+
for (const [index, element] of changed.entries()) fadeIn(element, this.motion.matches, 0 + index * 20);
|
|
233
233
|
}
|
|
234
234
|
markedText(text) {
|
|
235
235
|
if (typeof text === "string") return [text];
|
|
@@ -444,7 +444,7 @@ function fadeIn(element, reduced, delay) {
|
|
|
444
444
|
transform: "none"
|
|
445
445
|
}], {
|
|
446
446
|
delay: reduced ? 0 : delay,
|
|
447
|
-
duration: reduced ? 150 :
|
|
447
|
+
duration: reduced ? 150 : 140,
|
|
448
448
|
easing: ROW_EASE,
|
|
449
449
|
fill: "both"
|
|
450
450
|
});
|
package/dist/esm/card/prompt.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { styleNonce } from "../config.js";
|
|
2
2
|
import { useSystemVoiceMicrophone } from "../microphoneInput.js";
|
|
3
3
|
import { ROW_EASE } from "./rowMotion.js";
|
|
4
|
-
import { t as VoiceVisual } from "../chunks/visual-
|
|
4
|
+
import { t as VoiceVisual } from "../chunks/visual-rOVJCP2w.js";
|
|
5
5
|
//#region src/card/prompt.ts
|
|
6
6
|
const CSS = `
|
|
7
7
|
.seams-voiceid-activation-surface { width: min(100%, 26.25rem); margin: 0 auto; background: transparent; }
|
|
@@ -22,7 +22,9 @@ const CSS = `
|
|
|
22
22
|
margin: 0 auto;
|
|
23
23
|
padding: 3.5rem 1.5rem 1.5rem;
|
|
24
24
|
display: grid;
|
|
25
|
-
|
|
25
|
+
/* Named rows. The context row is empty (0 high) unless the card shows the text that its sentence
|
|
26
|
+
speaks for. A host that sets its own rows keeps these names. */
|
|
27
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] minmax(5rem, auto);
|
|
26
28
|
border: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
27
29
|
border-radius: var(--seams-shape-card, 16px);
|
|
28
30
|
background: var(--seams-colors-colorBackground, #192024);
|
|
@@ -54,12 +56,14 @@ const CSS = `
|
|
|
54
56
|
}
|
|
55
57
|
/* A card that holds focus itself (until its first control is ready, or after a confirm). */
|
|
56
58
|
.seams-voiceid-prompt[tabindex="-1"]:focus { outline: none; }
|
|
57
|
-
/* Each line owns its row. The download overlays rows
|
|
58
|
-
|
|
59
|
-
.seams-voiceid-prompt > .seams-voiceid-
|
|
60
|
-
.seams-voiceid-prompt >
|
|
61
|
-
.seams-voiceid-prompt > .seams-voiceid-
|
|
62
|
-
.seams-voiceid-
|
|
59
|
+
/* Each line owns its row. The download overlays the rows line, status and action; an auto-placed
|
|
60
|
+
line would move to new rows. */
|
|
61
|
+
.seams-voiceid-prompt > .seams-voiceid-context { grid-row: context; grid-column: 1; }
|
|
62
|
+
.seams-voiceid-prompt > h2 { grid-row: title; grid-column: 1; }
|
|
63
|
+
.seams-voiceid-prompt > .seams-voiceid-phrase { grid-row: phrase; grid-column: 1; }
|
|
64
|
+
.seams-voiceid-prompt > .seams-voiceid-ribbon { grid-row: line; grid-column: 1; }
|
|
65
|
+
.seams-voiceid-prompt > .seams-voiceid-status { grid-row: status; grid-column: 1; }
|
|
66
|
+
.seams-voiceid-progress-slot { grid-row: action; grid-column: 1; min-width: 0; align-self: center; }
|
|
63
67
|
.seams-voiceid-progress-slot:empty { visibility: hidden; }
|
|
64
68
|
.seams-voiceid-activation-surface .seams-voiceid-record-slot { transition: opacity 200ms cubic-bezier(0.2, 0, 0, 1) 80ms, visibility 0s linear 0s; }
|
|
65
69
|
.seams-voiceid-activation-surface[data-preparation="loading"] .seams-voiceid-record-slot { opacity: 0; visibility: hidden; transition: none; }
|
|
@@ -266,6 +270,12 @@ const CSS = `
|
|
|
266
270
|
.seams-voiceid-ribbon .seams-voiceid-ribbon-echo[data-layer="2"] {
|
|
267
271
|
color: var(--seams-colors-voiceEchoDeep, var(--seams-colors-voiceEcho, var(--seams-colors-voiceLine, currentColor)));
|
|
268
272
|
}
|
|
273
|
+
/* The pixel style (voiceLine: 'pixels'): dots in X blue tones by default, the lightest in the
|
|
274
|
+
middle row. The theme colours the three bands: voiceEcho (the middle row), voiceLine (rows
|
|
275
|
+
1 and 2 from it) and voiceEchoDeep (rows 3 and 4). */
|
|
276
|
+
.seams-voiceid-ribbon-pixel { fill: var(--seams-colors-voiceEchoDeep, #1a6fb0); }
|
|
277
|
+
.seams-voiceid-ribbon-pixel[data-band="1"] { fill: var(--seams-colors-voiceLine, #1d9bf0); }
|
|
278
|
+
.seams-voiceid-ribbon-pixel[data-band="0"] { fill: var(--seams-colors-voiceEcho, #8ecdf8); }
|
|
269
279
|
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-success-headline {
|
|
270
280
|
display: grid;
|
|
271
281
|
align-content: end;
|
|
@@ -276,6 +286,7 @@ const CSS = `
|
|
|
276
286
|
.seams-voiceid-prompt .seams-voiceid-status.seams-voiceid-success-message { font-size: 0.8125rem; }
|
|
277
287
|
/* The glow is decorative and cannot intercept record, retry or cancel controls. */
|
|
278
288
|
.seams-voiceid-glow { position: absolute; inset: 0; pointer-events: none; border-radius: inherit; z-index: 0; }
|
|
289
|
+
.seams-voiceid-glow[hidden] { display: none; }
|
|
279
290
|
.seams-voiceid-beam { width: 100%; height: 100%; border-radius: inherit; }
|
|
280
291
|
.seams-voiceid-prompt > :not(.seams-voiceid-glow) { position: relative; z-index: 1; }
|
|
281
292
|
.seams-voiceid-prompt > .seams-voiceid-close { position: absolute; }
|
|
@@ -313,8 +324,10 @@ const CSS = `
|
|
|
313
324
|
right: auto;
|
|
314
325
|
left: 0.75rem;
|
|
315
326
|
}
|
|
327
|
+
/* Minimize sits beside Close, in the trailing corner: one button width and a small gap in. */
|
|
328
|
+
.seams-voiceid-prompt .seams-voiceid-close.seams-voiceid-minimize { right: 3.375rem; }
|
|
316
329
|
.seams-voiceid-prompt .seams-voiceid-record-slot {
|
|
317
|
-
grid-row:
|
|
330
|
+
grid-row: action;
|
|
318
331
|
grid-column: 1;
|
|
319
332
|
align-self: center;
|
|
320
333
|
width: 100%;
|
|
@@ -396,7 +409,7 @@ const CSS = `
|
|
|
396
409
|
.seams-voiceid-prompt:has(.seams-voiceid-model-download:not([data-state="ready"])) > .seams-voiceid-phrase { opacity: 0.35; }
|
|
397
410
|
.seams-voiceid-prompt:has(.seams-voiceid-model-download[data-state="failed"]) > .seams-voiceid-record-slot { display: none; }
|
|
398
411
|
.seams-voiceid-model-download progress {
|
|
399
|
-
grid-row:
|
|
412
|
+
grid-row: line;
|
|
400
413
|
justify-self: center;
|
|
401
414
|
appearance: none;
|
|
402
415
|
display: block;
|
|
@@ -414,7 +427,7 @@ const CSS = `
|
|
|
414
427
|
a looping animation would compete with it on a software-rendered page. */
|
|
415
428
|
.seams-voiceid-model-download progress::-moz-progress-bar { background: var(--seams-colors-textPrimary, #eff2f3); border-radius: 999px; }
|
|
416
429
|
.seams-voiceid-model-download p {
|
|
417
|
-
grid-row:
|
|
430
|
+
grid-row: status;
|
|
418
431
|
align-self: start;
|
|
419
432
|
margin: 0;
|
|
420
433
|
font-size: 0.75rem;
|
|
@@ -422,9 +435,9 @@ const CSS = `
|
|
|
422
435
|
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
423
436
|
font-variant-numeric: tabular-nums;
|
|
424
437
|
}
|
|
425
|
-
.seams-voiceid-model-download button { grid-row:
|
|
438
|
+
.seams-voiceid-model-download button { grid-row: action; }
|
|
426
439
|
/* A phrase card shows its check and its result in the same card. */
|
|
427
|
-
.seams-voiceid-prompt > .seams-voiceid-status-line { grid-
|
|
440
|
+
.seams-voiceid-prompt > .seams-voiceid-status-line { grid-row: line; grid-column: 1; }
|
|
428
441
|
.seams-voiceid-status-line {
|
|
429
442
|
transition: opacity 120ms ease;
|
|
430
443
|
display: block;
|
|
@@ -434,7 +447,9 @@ const CSS = `
|
|
|
434
447
|
margin: auto;
|
|
435
448
|
overflow: visible;
|
|
436
449
|
opacity: 0;
|
|
437
|
-
|
|
450
|
+
/* The theme can colour the check's line and the seal that it closes into (the disc behind the
|
|
451
|
+
tick): voiceSeal, else the voice line's colour, else the card's text colour. */
|
|
452
|
+
color: var(--seams-colors-voiceSeal, var(--seams-colors-voiceLine, var(--seams-colors-textPrimary, #eff2f3)));
|
|
438
453
|
}
|
|
439
454
|
.seams-voiceid-ribbon { transition: opacity 120ms ease; }
|
|
440
455
|
.seams-voiceid-prompt[data-phrase-line="status"] > .seams-voiceid-ribbon { opacity: 0; }
|
|
@@ -474,17 +489,29 @@ const CSS = `
|
|
|
474
489
|
.seams-voiceid-prompt .seams-voiceid-phrase.seams-voiceid-phrase-dim { color: var(--seams-colors-textMuted, #6f7f87); }
|
|
475
490
|
.seams-voiceid-prompt[data-phrase-view] > h2 { min-height: 1.19rem; }
|
|
476
491
|
.seams-voiceid-prompt[data-phrase-view="check"] {
|
|
477
|
-
grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
|
|
492
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] 0;
|
|
478
493
|
}
|
|
479
494
|
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-record-slot { display: none; }
|
|
480
495
|
/* A card that records by itself (autoStart) has no Record row: its rows sit in the middle. */
|
|
481
496
|
.seams-voiceid-prompt[data-auto-start] {
|
|
482
|
-
grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) 0;
|
|
497
|
+
grid-template-rows: [context] auto [title] auto [phrase] minmax(6rem, auto) [line] 5rem [status] minmax(3.375rem, auto) [action] 0;
|
|
483
498
|
}
|
|
484
499
|
.seams-voiceid-prompt[data-auto-start] > .seams-voiceid-record-slot { display: none; }
|
|
500
|
+
/* The result reads from the top as one group: the seal, the headline under it, the caption and
|
|
501
|
+
the detail, then the button. Every row has its content's height, so the card has no empty
|
|
502
|
+
band, and the line row glides to its new place (phraseCard.ts transition). An empty label
|
|
503
|
+
takes no room. */
|
|
504
|
+
.seams-voiceid-prompt[data-phrase-view="result"] {
|
|
505
|
+
grid-template-rows: [context] auto [title] auto [line] 4.5rem [phrase] auto [status] auto [action] auto;
|
|
506
|
+
}
|
|
507
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > h2:empty { display: none; }
|
|
508
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-phrase { margin-top: 0.5rem; }
|
|
509
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-status { min-height: 0; margin-top: 0.375rem; }
|
|
510
|
+
.seams-voiceid-prompt[data-phrase-view="result"] > .seams-voiceid-record-slot { margin-top: 1.5rem; }
|
|
485
511
|
.seams-voiceid-prompt[data-phrase-view] .seams-voiceid-microphone { display: none; }
|
|
486
512
|
.seams-voiceid-prompt .seams-voiceid-close { transition: opacity 150ms ease, visibility 0s linear 0s; }
|
|
487
|
-
|
|
513
|
+
/* The check hides Back and Close, as the caller works; Minimize stays. */
|
|
514
|
+
.seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-close:not(.seams-voiceid-minimize) {
|
|
488
515
|
opacity: 0;
|
|
489
516
|
visibility: hidden;
|
|
490
517
|
transition: opacity 150ms ease, visibility 0s linear 150ms;
|
|
@@ -521,6 +548,213 @@ const CSS = `
|
|
|
521
548
|
@media (prefers-reduced-motion: reduce) {
|
|
522
549
|
.seams-voiceid-status-line rect { animation: none !important; transition: none; }
|
|
523
550
|
}
|
|
551
|
+
/* The note layout: a card with a context, in the shape of a form. The context (a note) sits
|
|
552
|
+
in a soft box under its label, the sentence to read sits centred in a blue box under its own
|
|
553
|
+
label, and one button right under it changes with the card: Record or the voice line while
|
|
554
|
+
it records, the check, then the result's action. The status sits at the foot, under the
|
|
555
|
+
button. */
|
|
556
|
+
.seams-voiceid-prompt[data-context] {
|
|
557
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [line] 4.5rem [action] 0 [status] auto;
|
|
558
|
+
text-align: start;
|
|
559
|
+
/* The height of the button: 4.5rem as the voice line, with either line style, and 3.5rem as
|
|
560
|
+
Record or a result's action. The microphone's circle follows it. */
|
|
561
|
+
--seams-voiceid-button-height: 4.5rem;
|
|
562
|
+
/* The fill of what the person says: a tint of the main button's colour (voiceFill). */
|
|
563
|
+
--seams-voiceid-tint: 16%;
|
|
564
|
+
--seams-voiceid-fill: var(--seams-colors-voiceFill, color-mix(in srgb, var(--seams-colors-buttonBackground, #1d9bf0) var(--seams-voiceid-tint), var(--seams-colors-colorBackground, #192024)));
|
|
565
|
+
}
|
|
566
|
+
/* A form is as wide as an app's dialog for a note: 560 px. */
|
|
567
|
+
.seams-voiceid-activation-surface:has(> .seams-voiceid-prompt[data-context]),
|
|
568
|
+
.seams-voiceid-prompt[data-context] {
|
|
569
|
+
width: min(100%, 35rem);
|
|
570
|
+
}
|
|
571
|
+
[data-theme="light"] .seams-voiceid-prompt[data-context] { --seams-voiceid-tint: 14%; }
|
|
572
|
+
[data-theme="dark"] .seams-voiceid-prompt[data-context] { --seams-voiceid-tint: 24%; }
|
|
573
|
+
/* A button (Record, Try again, a result's action) takes the place of the voice line. */
|
|
574
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) {
|
|
575
|
+
--seams-voiceid-button-height: 3.5rem;
|
|
576
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [line] 0 [action] auto [status] auto;
|
|
577
|
+
}
|
|
578
|
+
/* A result's caption and detail read before its button. */
|
|
579
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) {
|
|
580
|
+
grid-template-rows: [context] auto [title] auto [phrase] auto [status] auto [line] 0 [action] auto;
|
|
581
|
+
}
|
|
582
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="check"]):has(> .seams-voiceid-record-slot > .seams-voiceid-primary:not([hidden])) > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
583
|
+
visibility: hidden;
|
|
584
|
+
}
|
|
585
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-glow { display: none; }
|
|
586
|
+
/* A line across the card under the context: above it is what the person posts, below it is
|
|
587
|
+
the voice signature. */
|
|
588
|
+
.seams-voiceid-prompt .seams-voiceid-context {
|
|
589
|
+
margin: 0 -1.5rem 1.25rem;
|
|
590
|
+
padding: 0 1.5rem 1.25rem;
|
|
591
|
+
border: 0;
|
|
592
|
+
border-bottom: 1px solid var(--seams-colors-borderPrimary, #303b41);
|
|
593
|
+
}
|
|
594
|
+
.seams-voiceid-prompt .seams-voiceid-context-label {
|
|
595
|
+
display: block;
|
|
596
|
+
margin-bottom: 0.375rem;
|
|
597
|
+
color: var(--seams-colors-textSecondary, #9cabb3);
|
|
598
|
+
font-size: 0.9375rem;
|
|
599
|
+
line-height: 1.3;
|
|
600
|
+
}
|
|
601
|
+
.seams-voiceid-prompt .seams-voiceid-context > blockquote {
|
|
602
|
+
max-height: 8.5rem;
|
|
603
|
+
margin: 0;
|
|
604
|
+
padding: 0.75rem 1rem;
|
|
605
|
+
overflow-y: auto;
|
|
606
|
+
border-radius: 1rem;
|
|
607
|
+
background: var(--seams-colors-surface, #202b31);
|
|
608
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
609
|
+
font-size: 1rem;
|
|
610
|
+
line-height: 1.45;
|
|
611
|
+
white-space: pre-line;
|
|
612
|
+
overflow-wrap: anywhere;
|
|
613
|
+
}
|
|
614
|
+
/* A result has no label: its empty title takes no room. */
|
|
615
|
+
.seams-voiceid-prompt[data-context] > h2:empty { display: none; }
|
|
616
|
+
.seams-voiceid-prompt[data-context] > h2 {
|
|
617
|
+
margin: 0 0 0.625rem;
|
|
618
|
+
color: var(--seams-colors-textPrimary, #eff2f3);
|
|
619
|
+
font-size: 1rem;
|
|
620
|
+
font-weight: 700;
|
|
621
|
+
line-height: 1.3;
|
|
622
|
+
text-align: center;
|
|
623
|
+
}
|
|
624
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-phrase {
|
|
625
|
+
box-sizing: border-box;
|
|
626
|
+
width: 100%;
|
|
627
|
+
max-width: none;
|
|
628
|
+
/* A thin gap to the button: the sentence and the voice belong together. */
|
|
629
|
+
margin: 0 0 0.5rem;
|
|
630
|
+
padding: 0.875rem 1.25rem;
|
|
631
|
+
border-radius: 1rem;
|
|
632
|
+
/* Blue, apart from the grey box of the context: this is what the person says. */
|
|
633
|
+
background: var(--seams-voiceid-fill);
|
|
634
|
+
font-size: 1.25rem;
|
|
635
|
+
font-weight: 600;
|
|
636
|
+
letter-spacing: normal;
|
|
637
|
+
line-height: 1.35;
|
|
638
|
+
text-align: center;
|
|
639
|
+
}
|
|
640
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-phrase.seams-voiceid-success-headline {
|
|
641
|
+
display: block;
|
|
642
|
+
padding: 0;
|
|
643
|
+
background: none;
|
|
644
|
+
font-size: 1.25rem;
|
|
645
|
+
font-weight: 700;
|
|
646
|
+
}
|
|
647
|
+
/* The status sits at the foot of the card, under the button: "Listening", or the check. */
|
|
648
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status {
|
|
649
|
+
min-height: 1.125rem;
|
|
650
|
+
margin: 0;
|
|
651
|
+
padding: 0.625rem 0 0;
|
|
652
|
+
text-align: center;
|
|
653
|
+
}
|
|
654
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"] .seams-voiceid-status { padding: 0.25rem 0 0.75rem; }
|
|
655
|
+
/* Under the button, the check's caption and detail share one line at the size of "Listening",
|
|
656
|
+
so the card keeps its height when the check starts. A long line ends with an ellipsis. */
|
|
657
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status.seams-voiceid-two-lines {
|
|
658
|
+
overflow: hidden;
|
|
659
|
+
white-space: nowrap;
|
|
660
|
+
text-overflow: ellipsis;
|
|
661
|
+
}
|
|
662
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status.seams-voiceid-two-lines > span {
|
|
663
|
+
display: inline;
|
|
664
|
+
}
|
|
665
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status .seams-voiceid-caption {
|
|
666
|
+
margin: 0;
|
|
667
|
+
font-size: inherit;
|
|
668
|
+
}
|
|
669
|
+
.seams-voiceid-prompt[data-context]:not([data-phrase-view="result"]) .seams-voiceid-status .seams-voiceid-caption-detail:not(:empty)::before {
|
|
670
|
+
content: ' · ';
|
|
671
|
+
}
|
|
672
|
+
/* A result's link, such as the transaction in a block explorer: short, in the main button's
|
|
673
|
+
colour, underlined only on hover. */
|
|
674
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a {
|
|
675
|
+
color: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
676
|
+
font-weight: 500;
|
|
677
|
+
text-decoration: none;
|
|
678
|
+
}
|
|
679
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a:hover,
|
|
680
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status a:focus-visible { text-decoration: underline; }
|
|
681
|
+
/* The voice line and the check, in the main button's colours, with either line style. The line
|
|
682
|
+
keeps its drawing size; the taller button gives it room above and below. */
|
|
683
|
+
.seams-voiceid-prompt[data-context] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
684
|
+
box-sizing: border-box;
|
|
685
|
+
width: 100%;
|
|
686
|
+
height: 4.5rem;
|
|
687
|
+
padding-block: 0.5rem;
|
|
688
|
+
overflow: hidden;
|
|
689
|
+
border-radius: 9999px;
|
|
690
|
+
background: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
691
|
+
color: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
692
|
+
}
|
|
693
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon .seams-voiceid-ribbon-echo { color: currentColor; }
|
|
694
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect {
|
|
695
|
+
fill: color-mix(in srgb, currentColor 35%, transparent);
|
|
696
|
+
}
|
|
697
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect[data-state="done"],
|
|
698
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-line rect[data-state="run"] { fill: currentColor; }
|
|
699
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-status-check {
|
|
700
|
+
stroke: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
701
|
+
}
|
|
702
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] {
|
|
703
|
+
--seams-voiceid-pixel-background: var(--seams-colors-voicePixelBackground, var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3)));
|
|
704
|
+
--seams-voiceid-pixel-ink: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
705
|
+
}
|
|
706
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
707
|
+
background: var(--seams-voiceid-pixel-background);
|
|
708
|
+
color: var(--seams-voiceid-pixel-ink);
|
|
709
|
+
}
|
|
710
|
+
.seams-voiceid-prompt[data-context][data-voice-line="pixels"] .seams-voiceid-status-check {
|
|
711
|
+
stroke: var(--seams-voiceid-pixel-background);
|
|
712
|
+
}
|
|
713
|
+
/* On the button, the dots are the button's text colour: full in the middle row, then mixed a
|
|
714
|
+
little and more with the button's colour (white, light and light blue on X blue). */
|
|
715
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="0"] { fill: var(--seams-voiceid-pixel-ink); }
|
|
716
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="1"] {
|
|
717
|
+
fill: color-mix(in srgb, var(--seams-voiceid-pixel-ink) 80%, var(--seams-voiceid-pixel-background));
|
|
718
|
+
}
|
|
719
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-ribbon-pixel[data-band="2"] {
|
|
720
|
+
fill: color-mix(in srgb, var(--seams-voiceid-pixel-ink) 55%, var(--seams-voiceid-pixel-background));
|
|
721
|
+
}
|
|
722
|
+
/* The microphone at the left of the button, as on an app's own voice button: on Record, on the
|
|
723
|
+
voice line while the card records, and on the check. A result's button has none. Its circle
|
|
724
|
+
keeps the same gap to the button's top, left and bottom, so it follows the button's round end.
|
|
725
|
+
The line leaves the icon its room. */
|
|
726
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-line-icon {
|
|
727
|
+
--seams-voiceid-icon-gap: 0.375rem;
|
|
728
|
+
grid-row: line / span 2;
|
|
729
|
+
grid-column: 1;
|
|
730
|
+
align-self: center;
|
|
731
|
+
justify-self: start;
|
|
732
|
+
z-index: 2;
|
|
733
|
+
display: grid;
|
|
734
|
+
place-items: center;
|
|
735
|
+
width: calc(var(--seams-voiceid-button-height) - 2 * var(--seams-voiceid-icon-gap));
|
|
736
|
+
height: calc(var(--seams-voiceid-button-height) - 2 * var(--seams-voiceid-icon-gap));
|
|
737
|
+
margin-inline-start: var(--seams-voiceid-icon-gap);
|
|
738
|
+
border-radius: 50%;
|
|
739
|
+
transition: width 180ms ease, height 180ms ease;
|
|
740
|
+
background: var(--seams-colors-textButton, var(--seams-colors-colorBackground, #192024));
|
|
741
|
+
color: var(--seams-colors-buttonBackground, var(--seams-colors-textPrimary, #eff2f3));
|
|
742
|
+
pointer-events: none;
|
|
743
|
+
}
|
|
744
|
+
.seams-voiceid-line-icon > svg { width: 46%; height: 46%; }
|
|
745
|
+
.seams-voiceid-prompt[data-context][data-phrase-view="result"] > .seams-voiceid-line-icon { display: none; }
|
|
746
|
+
.seams-voiceid-prompt[data-context] > :is(.seams-voiceid-ribbon, .seams-voiceid-status-line) {
|
|
747
|
+
box-sizing: border-box;
|
|
748
|
+
padding-inline: calc(var(--seams-voiceid-button-height) + 0.25rem) 1rem;
|
|
749
|
+
}
|
|
750
|
+
/* The status line's own space below it is the gap above the button, in every view. */
|
|
751
|
+
.seams-voiceid-prompt[data-context] > .seams-voiceid-record-slot { margin-top: 0; }
|
|
752
|
+
.seams-voiceid-prompt[data-context] .seams-voiceid-primary {
|
|
753
|
+
min-height: 3.5rem;
|
|
754
|
+
border-radius: 9999px;
|
|
755
|
+
font-size: 1.0625rem;
|
|
756
|
+
font-weight: 700;
|
|
757
|
+
}
|
|
524
758
|
@media (hover: hover) and (pointer: fine) {
|
|
525
759
|
.seams-voiceid-close:hover { background: var(--seams-colors-surface, #202b31); }
|
|
526
760
|
.seams-voiceid-primary:hover:not(:disabled) { opacity: 0.9; }
|
|
@@ -611,6 +845,29 @@ var VoicePrompt = class {
|
|
|
611
845
|
this.element.append(this.visual.glow, this.cancel, this.title, this.phrase, this.visual.element, this.status, slot, this.progress);
|
|
612
846
|
this.sizes.observe(this.element);
|
|
613
847
|
}
|
|
848
|
+
/** Draws the voice as wave lines (the default) or as columns of pixels. */
|
|
849
|
+
useVoiceLine(style) {
|
|
850
|
+
this.element.dataset.voiceLine = style;
|
|
851
|
+
this.visual.setLineStyle(style);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Shows the text that the sentence speaks for, such as the note that a spoken summary signs,
|
|
855
|
+
* above the sentence. It stays for the check and the result.
|
|
856
|
+
*/
|
|
857
|
+
showContext(label, text) {
|
|
858
|
+
const document = this.element.ownerDocument;
|
|
859
|
+
const context = document.createElement("figure");
|
|
860
|
+
context.className = "seams-voiceid-context";
|
|
861
|
+
const caption = document.createElement("figcaption");
|
|
862
|
+
caption.className = "seams-voiceid-context-label";
|
|
863
|
+
caption.textContent = label;
|
|
864
|
+
const body = document.createElement("blockquote");
|
|
865
|
+
body.textContent = text;
|
|
866
|
+
context.append(caption, body);
|
|
867
|
+
this.title.before(context);
|
|
868
|
+
this.element.append(microphoneIcon(document));
|
|
869
|
+
this.element.dataset.context = "";
|
|
870
|
+
}
|
|
614
871
|
/**
|
|
615
872
|
* In an app's dialog with a step before this card, Back in the leading corner returns to that
|
|
616
873
|
* step. With `close`, Close stays in the trailing corner and ends the whole dialog; without it,
|
|
@@ -630,6 +887,16 @@ var VoicePrompt = class {
|
|
|
630
887
|
return this.cancel;
|
|
631
888
|
}
|
|
632
889
|
/**
|
|
890
|
+
* Minimize in the trailing corner, beside Close. It stays in the check, where Close is hidden,
|
|
891
|
+
* so the person can hide the card while the check goes on. The result is the control.
|
|
892
|
+
*/
|
|
893
|
+
showMinimize() {
|
|
894
|
+
const minimize = cornerButton(this.element.ownerDocument, "Minimize", MINIMIZE_PATH);
|
|
895
|
+
minimize.classList.add("seams-voiceid-minimize");
|
|
896
|
+
this.cancel.before(minimize);
|
|
897
|
+
return minimize;
|
|
898
|
+
}
|
|
899
|
+
/**
|
|
633
900
|
* Moves the rows to a new state. The rows whose content changes fade out together, take their
|
|
634
901
|
* new content and fade in one after another; the other rows stay still. A change that comes
|
|
635
902
|
* during a fade joins it, so no state in between is drawn.
|
|
@@ -1034,8 +1301,8 @@ function fadeRowsIn(rows) {
|
|
|
1034
1301
|
opacity: to,
|
|
1035
1302
|
transform: "none"
|
|
1036
1303
|
}], {
|
|
1037
|
-
delay: reduced ? 0 :
|
|
1038
|
-
duration: reduced ? 150 :
|
|
1304
|
+
delay: reduced ? 0 : 0 + index * 20,
|
|
1305
|
+
duration: reduced ? 150 : 140,
|
|
1039
1306
|
easing: ROW_EASE,
|
|
1040
1307
|
fill: "backwards",
|
|
1041
1308
|
id: ROW_MOTION
|
|
@@ -1045,7 +1312,7 @@ function fadeRowsIn(rows) {
|
|
|
1045
1312
|
function ignoreCancel() {}
|
|
1046
1313
|
function continueFade({ row, opacity }) {
|
|
1047
1314
|
row.animate([{ opacity }, { opacity: 1 }], {
|
|
1048
|
-
duration:
|
|
1315
|
+
duration: 140 * (1 - opacity),
|
|
1049
1316
|
easing: ROW_EASE,
|
|
1050
1317
|
id: ROW_MOTION
|
|
1051
1318
|
});
|
|
@@ -1078,6 +1345,34 @@ function leaveSurface(element) {
|
|
|
1078
1345
|
}
|
|
1079
1346
|
const CLOSE_PATH = "M6 6l12 12M18 6L6 18";
|
|
1080
1347
|
const BACK_PATH = "m15 18-6-6 6-6";
|
|
1348
|
+
const MINIMIZE_PATH = "M6 12h12";
|
|
1349
|
+
/** A microphone: its head, its stand and its foot. */
|
|
1350
|
+
const MICROPHONE_PATHS = [
|
|
1351
|
+
"M9 5a3 3 0 0 1 6 0v5a3 3 0 0 1-6 0z",
|
|
1352
|
+
"M5 10a7 7 0 0 0 14 0",
|
|
1353
|
+
"M8 21h8",
|
|
1354
|
+
"M12 17v4"
|
|
1355
|
+
];
|
|
1356
|
+
/** The microphone at the left of a form card's button, in its own circle. */
|
|
1357
|
+
function microphoneIcon(document) {
|
|
1358
|
+
const circle = document.createElement("span");
|
|
1359
|
+
circle.className = "seams-voiceid-line-icon";
|
|
1360
|
+
circle.setAttribute("aria-hidden", "true");
|
|
1361
|
+
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
|
1362
|
+
icon.setAttribute("viewBox", "0 0 24 24");
|
|
1363
|
+
for (const d of MICROPHONE_PATHS) {
|
|
1364
|
+
const part = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
|
1365
|
+
part.setAttribute("d", d);
|
|
1366
|
+
part.setAttribute("fill", "none");
|
|
1367
|
+
part.setAttribute("stroke", "currentColor");
|
|
1368
|
+
part.setAttribute("stroke-width", "1.8");
|
|
1369
|
+
part.setAttribute("stroke-linecap", "round");
|
|
1370
|
+
part.setAttribute("stroke-linejoin", "round");
|
|
1371
|
+
icon.append(part);
|
|
1372
|
+
}
|
|
1373
|
+
circle.append(icon);
|
|
1374
|
+
return circle;
|
|
1375
|
+
}
|
|
1081
1376
|
/** A control in a corner of the card, with an icon and no text. */
|
|
1082
1377
|
function cornerButton(document, label, path) {
|
|
1083
1378
|
const button = document.createElement("button");
|
package/dist/esm/card/ribbon.js
CHANGED
|
@@ -31,6 +31,19 @@ const LINE_REST_OPACITY = .55;
|
|
|
31
31
|
const SUCCESS_MS = 1140;
|
|
32
32
|
const SEAL_RADIUS = 22.5;
|
|
33
33
|
const CHECK_PATH = "M161 26.8 151.4 36.3 147.1 32";
|
|
34
|
+
const PIXEL_COLUMNS = 44;
|
|
35
|
+
/** The rows above the middle row, and the rows below it. */
|
|
36
|
+
const PIXEL_ROWS = 4;
|
|
37
|
+
const PIXEL_PITCH = 7;
|
|
38
|
+
/** Rows sit closer than columns, so the tallest column keeps a gap to the edge of a button. */
|
|
39
|
+
const PIXEL_ROW_PITCH = 6;
|
|
40
|
+
const PIXEL_SIZE = 4.5;
|
|
41
|
+
const PIXEL_STEP_MS = 70;
|
|
42
|
+
/** A column shows between this share of the level and all of it. */
|
|
43
|
+
const PIXEL_SPREAD = .3;
|
|
44
|
+
const PIXEL_IDLE_LEVEL = .18;
|
|
45
|
+
const PIXEL_IDLE_SWING = .12;
|
|
46
|
+
const PIXEL_IDLE_PERIOD_MS = 2400;
|
|
34
47
|
const easeMove = cubicBezier(.65, 0, .35, 1);
|
|
35
48
|
const easeArc = cubicBezier(.4, 0, .2, 1);
|
|
36
49
|
const easeSeal = cubicBezier(.34, 1.56, .64, 1);
|
|
@@ -57,6 +70,14 @@ var VoiceRibbon = class {
|
|
|
57
70
|
target = 0;
|
|
58
71
|
level = 0;
|
|
59
72
|
stopped = false;
|
|
73
|
+
lineStyle = "wave";
|
|
74
|
+
/** The pixel style's dots by column, top row first. Made when the style is first chosen. */
|
|
75
|
+
pixels = null;
|
|
76
|
+
/** The level of each column, the newest at the right end. */
|
|
77
|
+
pixelLevels = new Array(PIXEL_COLUMNS).fill(0);
|
|
78
|
+
/** The rows that each column shows now, above and below the middle row. */
|
|
79
|
+
pixelHeights = new Array(PIXEL_COLUMNS).fill(-1);
|
|
80
|
+
pixelClock = 0;
|
|
60
81
|
constructor(document, checking = "ring") {
|
|
61
82
|
this.checking = checking;
|
|
62
83
|
this.element = document.createElementNS(SVG_NAMESPACE, "svg");
|
|
@@ -103,6 +124,46 @@ var VoiceRibbon = class {
|
|
|
103
124
|
this.paint(performance.now());
|
|
104
125
|
}
|
|
105
126
|
readLevel = () => this.level;
|
|
127
|
+
/** Draws the voice as wave lines (the default) or as columns of pixels. */
|
|
128
|
+
setLineStyle(style) {
|
|
129
|
+
this.lineStyle = style;
|
|
130
|
+
this.element.dataset.lineStyle = style;
|
|
131
|
+
if (style === "pixels" && !this.pixels) this.pixels = this.makePixels();
|
|
132
|
+
this.pixelHeights.fill(-1);
|
|
133
|
+
this.paint(performance.now());
|
|
134
|
+
this.wake();
|
|
135
|
+
}
|
|
136
|
+
makePixels() {
|
|
137
|
+
const document = this.element.ownerDocument;
|
|
138
|
+
const group = document.createElementNS(SVG_NAMESPACE, "g");
|
|
139
|
+
group.classList.add("seams-voiceid-ribbon-pixels");
|
|
140
|
+
group.setAttribute("opacity", "0");
|
|
141
|
+
const left = (WIDTH - ((PIXEL_COLUMNS - 1) * PIXEL_PITCH + PIXEL_SIZE)) / 2;
|
|
142
|
+
const columns = [];
|
|
143
|
+
for (let column = 0; column < PIXEL_COLUMNS; column += 1) {
|
|
144
|
+
const dots = [];
|
|
145
|
+
for (let row = -PIXEL_ROWS; row <= PIXEL_ROWS; row += 1) {
|
|
146
|
+
const dot = document.createElementNS(SVG_NAMESPACE, "rect");
|
|
147
|
+
dot.classList.add("seams-voiceid-ribbon-pixel");
|
|
148
|
+
const distance = Math.abs(row);
|
|
149
|
+
dot.dataset.band = distance === 0 ? "0" : distance <= 2 ? "1" : "2";
|
|
150
|
+
dot.setAttribute("x", (left + column * PIXEL_PITCH).toFixed(2));
|
|
151
|
+
dot.setAttribute("y", (MID - PIXEL_SIZE / 2 + row * PIXEL_ROW_PITCH).toFixed(2));
|
|
152
|
+
dot.setAttribute("width", String(PIXEL_SIZE));
|
|
153
|
+
dot.setAttribute("height", String(PIXEL_SIZE));
|
|
154
|
+
dot.setAttribute("rx", "1");
|
|
155
|
+
dot.setAttribute("opacity", "0");
|
|
156
|
+
group.append(dot);
|
|
157
|
+
dots.push(dot);
|
|
158
|
+
}
|
|
159
|
+
columns.push(dots);
|
|
160
|
+
}
|
|
161
|
+
this.element.prepend(group);
|
|
162
|
+
return {
|
|
163
|
+
group,
|
|
164
|
+
columns
|
|
165
|
+
};
|
|
166
|
+
}
|
|
106
167
|
/** The voice line's width now: 1, or the theme's `voiceLineWidth`. */
|
|
107
168
|
lineWidth() {
|
|
108
169
|
const line = this.lines[this.lines.length - 1];
|
|
@@ -188,13 +249,23 @@ var VoiceRibbon = class {
|
|
|
188
249
|
this.previousTime = time;
|
|
189
250
|
this.level += (this.target - this.level) * (1 - Math.exp(-dt / (this.target > this.level ? 55 : 200)));
|
|
190
251
|
if (!this.motion.matches) this.position += dt * (.0012 + this.level * .003);
|
|
252
|
+
if (this.lineStyle === "pixels") this.stepPixels(dt);
|
|
191
253
|
this.paint(time);
|
|
192
|
-
if (!(!this.element.isConnected || this.stage.kind === "sealed" || this.stage.kind === "line" && this.phase === "idle" && this.level <= .001)) this.frame = requestAnimationFrame(this.draw);
|
|
254
|
+
if (!(!this.element.isConnected || this.stage.kind === "sealed" || this.stage.kind === "line" && this.phase === "idle" && this.level <= .001 && this.pixelLevels.every((level) => level <= .001))) this.frame = requestAnimationFrame(this.draw);
|
|
193
255
|
};
|
|
194
256
|
idleAmplitude(time) {
|
|
195
257
|
if (this.phase !== "listening" || this.motion.matches) return 0;
|
|
196
258
|
return IDLE_AMPLITUDE * (.72 + .28 * Math.sin(time / IDLE_PERIOD_MS * TAU));
|
|
197
259
|
}
|
|
260
|
+
/** Adds the level of each step at the right end; the older columns move left. */
|
|
261
|
+
stepPixels(dt) {
|
|
262
|
+
this.pixelClock += dt;
|
|
263
|
+
while (this.pixelClock >= PIXEL_STEP_MS) {
|
|
264
|
+
this.pixelClock -= PIXEL_STEP_MS;
|
|
265
|
+
this.pixelLevels.shift();
|
|
266
|
+
this.pixelLevels.push(this.level * (PIXEL_SPREAD + (1 - PIXEL_SPREAD) * Math.random()));
|
|
267
|
+
}
|
|
268
|
+
}
|
|
198
269
|
paint(time) {
|
|
199
270
|
for (const part of [
|
|
200
271
|
this.ripple,
|
|
@@ -206,9 +277,11 @@ var VoiceRibbon = class {
|
|
|
206
277
|
this.check
|
|
207
278
|
]) part.setAttribute("opacity", "0");
|
|
208
279
|
for (const line of this.lines) line.setAttribute("opacity", "0");
|
|
280
|
+
this.pixels?.group.setAttribute("opacity", "0");
|
|
209
281
|
switch (this.stage.kind) {
|
|
210
282
|
case "line":
|
|
211
|
-
|
|
283
|
+
if (this.lineStyle === "pixels") this.paintPixels(time);
|
|
284
|
+
else this.paintLine(Math.max(this.level, this.idleAmplitude(time)));
|
|
212
285
|
return;
|
|
213
286
|
case "capture":
|
|
214
287
|
this.paintCapture(time, this.stage);
|
|
@@ -237,6 +310,23 @@ var VoiceRibbon = class {
|
|
|
237
310
|
this.lines[index].setAttribute("opacity", String(layer === 0 ? LINE_REST_OPACITY + amplitude * .4 : .1 + amplitude * .1));
|
|
238
311
|
}
|
|
239
312
|
}
|
|
313
|
+
/** Each column shows the middle dot and as many rows above and below it as its level. */
|
|
314
|
+
paintPixels(time) {
|
|
315
|
+
if (!this.pixels) return;
|
|
316
|
+
this.pixels.group.setAttribute("opacity", "1");
|
|
317
|
+
const live = this.phase === "listening" && !this.motion.matches;
|
|
318
|
+
for (const [column, dots] of this.pixels.columns.entries()) {
|
|
319
|
+
let level = this.pixelLevels[column] ?? 0;
|
|
320
|
+
if (live) {
|
|
321
|
+
const wave = Math.sin(time / PIXEL_IDLE_PERIOD_MS * TAU - column * .55);
|
|
322
|
+
level = Math.max(level, PIXEL_IDLE_LEVEL + PIXEL_IDLE_SWING * wave);
|
|
323
|
+
}
|
|
324
|
+
const height = Math.min(PIXEL_ROWS, Math.round(level * PIXEL_ROWS * 1.3));
|
|
325
|
+
if (height === this.pixelHeights[column]) continue;
|
|
326
|
+
this.pixelHeights[column] = height;
|
|
327
|
+
for (const [index, dot] of dots.entries()) dot.setAttribute("opacity", Math.abs(index - PIXEL_ROWS) <= height ? "1" : "0");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
240
330
|
paintSegments(time, stage) {
|
|
241
331
|
const elapsed = time - stage.startedAt;
|
|
242
332
|
if (!this.motion.matches && elapsed < SPLIT_FLATTEN_MS) {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
//#region src/card/rowMotion.ts
|
|
2
2
|
const ROW_OUT_MS = 150;
|
|
3
|
-
const ROW_IN_MS =
|
|
4
|
-
const ROW_IN_DELAY_MS =
|
|
5
|
-
const ROW_STAGGER_MS =
|
|
3
|
+
const ROW_IN_MS = 140;
|
|
4
|
+
const ROW_IN_DELAY_MS = 0;
|
|
5
|
+
const ROW_STAGGER_MS = 20;
|
|
6
6
|
const ROW_EASE = "cubic-bezier(0.2, 0, 0, 1)";
|
|
7
7
|
//#endregion
|
|
8
8
|
export { ROW_EASE, ROW_IN_DELAY_MS, ROW_IN_MS, ROW_OUT_MS, ROW_STAGGER_MS };
|