@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.
@@ -1,5 +1,5 @@
1
1
  import { PhraseCard } from "./phraseCard.js";
2
- import { t as VoiceVisual } from "../chunks/visual-CeEQ8QkM.js";
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, 20 + index * 30);
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 : 200,
447
+ duration: reduced ? 150 : 140,
448
448
  easing: ROW_EASE,
449
449
  fill: "both"
450
450
  });
@@ -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-CeEQ8QkM.js";
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
- grid-template-rows: auto minmax(6rem, auto) 5rem minmax(3.375rem, auto) minmax(5rem, auto);
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 3-5; an auto-placed line would move to new rows. */
58
- .seams-voiceid-prompt > h2 { grid-area: 1 / 1; }
59
- .seams-voiceid-prompt > .seams-voiceid-phrase { grid-area: 2 / 1; }
60
- .seams-voiceid-prompt > .seams-voiceid-ribbon { grid-area: 3 / 1; }
61
- .seams-voiceid-prompt > .seams-voiceid-status { grid-area: 4 / 1; }
62
- .seams-voiceid-progress-slot { grid-row: 5; grid-column: 1; min-width: 0; align-self: center; }
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: 5;
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: 3;
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: 4;
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: 5; }
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-area: 3 / 1; }
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
- color: var(--seams-colors-textPrimary, #eff2f3);
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
- .seams-voiceid-prompt[data-phrase-view="check"] .seams-voiceid-close {
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 : 20 + index * 30,
1038
- duration: reduced ? 150 : 200,
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: 200 * (1 - opacity),
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");
@@ -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
- this.paintLine(Math.max(this.level, this.idleAmplitude(time)));
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 = 200;
4
- const ROW_IN_DELAY_MS = 20;
5
- const ROW_STAGGER_MS = 30;
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 };