@telepath-computer/television 0.1.74 → 0.1.76

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.
@@ -74,6 +74,15 @@ select {
74
74
  --text-xl: round(calc(var(--text-lg) * var(--text-scale)), 1px);
75
75
  --leading-base: 1.5;
76
76
 
77
+ /* Control height — single source of truth for buttons, segmented-controls,
78
+ inputs and any future control whose shape includes "horizontal pill".
79
+ Suffix names a size (md = default); add `--control-height-sm`,
80
+ `--control-height-lg` if/when those sizes appear. Setting the height
81
+ directly (vs. computing from padding + line-height) is what lets a
82
+ mixed row of text + icon-only chrome line up exactly. */
83
+ --control-height-sm: 24px;
84
+ --control-height-md: 28px;
85
+
77
86
  /* Spacing */
78
87
  --space-2: 2px;
79
88
  --space-4: 4px;
@@ -379,19 +388,30 @@ th {
379
388
  either light or dark surfaces. */
380
389
  input,
381
390
  textarea {
382
- padding: 6px 10px;
383
391
  border: 0.5px solid var(--tint-300);
384
392
  border-radius: var(--radius-12);
385
393
  background: var(--tint-100);
386
394
  color: inherit;
387
395
  font: inherit;
396
+ box-sizing: border-box;
388
397
  transition:
389
398
  background 120ms ease,
390
399
  border-color 120ms ease,
391
400
  box-shadow 120ms ease;
392
401
  }
393
402
 
403
+ /* Single-line input snaps to the shared control height so it lines up
404
+ with buttons in a chrome row. Padding goes horizontal-only since the
405
+ explicit height handles vertical centering via the line-box. */
406
+ input {
407
+ height: var(--control-height-md);
408
+ padding: 0 10px;
409
+ }
410
+
411
+ /* Multi-line textarea stays intrinsic — height grows with content (or
412
+ max-height on a host) rather than locking to a control unit. */
394
413
  textarea {
414
+ padding: 6px 10px;
395
415
  resize: none;
396
416
  }
397
417
 
@@ -434,26 +454,53 @@ label.field > span {
434
454
  }
435
455
  }
436
456
 
457
+ /* Button-only layout — placement of icon + label inside a single
458
+ button. Segmented-button doesn't share these (it's a flex row of
459
+ children, not a center-aligned label-and-icon stack).
460
+ `height: var(--control-height-md)` is explicit so a row of mixed
461
+ chrome (text button + icon-only button + segmented-control) lines up
462
+ without each variant having to tune its intrinsic content + padding
463
+ to match. Padding is purely horizontal — vertical centering is handled
464
+ by `align-items: center`. */
437
465
  button {
438
466
  display: inline-flex;
439
467
  align-items: center;
440
468
  justify-content: center;
441
469
  gap: var(--space-6);
442
- padding: 4px 14px;
443
- border: 0.5px solid var(--tint-200);
444
- border-radius: var(--radius-12);
445
- background: var(--color-surface);
446
- color: inherit;
470
+ box-sizing: border-box;
471
+ height: var(--control-height-md);
472
+ /* 2px extra bottom padding compensates for the visual offset from
473
+ font ascender/descender asymmetry — `align-items: center` centers
474
+ the line-box but the glyph mass sits below center because the
475
+ ascender claims more vertical space than the descender. The
476
+ icon-only override resets padding to 0 since icons are already
477
+ symmetric. */
478
+ padding: 0 14px 2px;
447
479
  font: inherit;
448
480
  cursor: default;
481
+ }
482
+
483
+ /* Default chrome — shared between <button> and <segmented-control> so
484
+ the two stay in visual lockstep. Variant overrides (ghost, primary,
485
+ danger) and tone-dark / tone-light / disabled cascades live below.
486
+ The shared-with-segmented bits are intentional: a segmented control
487
+ is a chrome surface that looks like a single button containing two
488
+ click targets, so its host adopts the same fill, hairline, radius,
489
+ and shadow. */
490
+ button,
491
+ segmented-control {
492
+ background: var(--color-surface);
493
+ border: 0.5px solid var(--tint-200);
494
+ border-radius: var(--radius-12);
449
495
  box-shadow: var(--shadow-control);
496
+ color: inherit;
450
497
  }
451
498
 
452
- button:hover:not(:disabled) {
499
+ button:not([material]):hover:not(:disabled) {
453
500
  background: color-mix(in srgb, var(--color-surface) 96%, black);
454
501
  }
455
502
 
456
- button:active:not(:disabled) {
503
+ button:not([material]):active:not(:disabled) {
457
504
  background: color-mix(in srgb, var(--color-surface) 88%, black);
458
505
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.08);
459
506
  }
@@ -503,7 +550,7 @@ button[variant="ghost"] {
503
550
  /* Ghost is a chromeless overlay — at rest it's invisible and relies on
504
551
  the surface behind it. The overlay tokens cascade through `tone="dark"`
505
552
  (set on the button itself by the auto-tone watcher in
506
- `elements/button.ts`, or inherited from a tone-sampled ancestor), so
553
+ `utils/button-tone.ts`, or inherited from a tone-sampled ancestor), so
507
554
  one rule covers light + dark backdrops. */
508
555
  button[variant="ghost"]:hover:not(:disabled) {
509
556
  background: var(--color-overlay-hover);
@@ -527,6 +574,81 @@ button[variant="ghost"][aria-expanded="true"]:hover:not(:disabled) {
527
574
  box-shadow: none;
528
575
  }
529
576
 
577
+ /* Glass variant — translucent surface with backdrop blur. Shared between
578
+ <button variant="glass"> and <segmented-control variant="glass"> so a
579
+ chrome row of buttons + segmented controls reads as one surface
580
+ family. Self-contained per-variant chrome (bg + border + shadow + blur
581
+ + hover/active overlays) means no fighting with default-button rules
582
+ or tone-driven default fills.
583
+
584
+ No explicit `color` here — auto-tone (utils/button-tone.ts) writes
585
+ `tone="light"` or `tone="dark"` on the element after sampling its
586
+ actual backdrop. The materials.css `[tone="…"]` color rules then set
587
+ text color on the element directly (specificity 0,1,0), beating the
588
+ shared `button { color: inherit }` (0,0,1). An explicit `color:
589
+ inherit` here would be 0,1,1 and would defeat the tone-aware color. */
590
+ button[variant="glass"],
591
+ segmented-control[variant="glass"] {
592
+ background: transparent;
593
+ border: 0.5px solid color-mix(in srgb, var(--black) var(--alpha-100), transparent);
594
+ backdrop-filter: blur(20px) brightness(0.95) saturate(1.5);
595
+ -webkit-backdrop-filter: blur(20px) brightness(0.95) saturate(1.5);
596
+ box-shadow: none;
597
+ }
598
+
599
+ /* `background:` shorthand (not `background-image`) resets background-color
600
+ back to transparent — necessary because the default-button hover rule
601
+ `button:not([material]):hover` fires on glass too (glass has no
602
+ material) at equal specificity and would otherwise paint a 96% white
603
+ surface underneath this overlay. */
604
+ button[variant="glass"]:hover:not(:disabled) {
605
+ background: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
606
+ }
607
+
608
+ button[variant="glass"]:active:not(:disabled) {
609
+ background: linear-gradient(var(--color-overlay-active), var(--color-overlay-active));
610
+ }
611
+
612
+ [tone="dark"] button[variant="glass"],
613
+ [tone="dark"] segmented-control[variant="glass"] {
614
+ border-color: var(--tint-400);
615
+ }
616
+
617
+ /* Material composition with controls (option B in the design notes —
618
+ contained override rather than refactoring materials.css globally).
619
+ Material is a passive-surface primitive: it locks color, draws a
620
+ fixed-alpha hairline, and a default-variant `:hover` would replace
621
+ its background entirely. These overrides let `<button material="...">`
622
+ and `<segmented-control material="...">` compose:
623
+
624
+ - `color: inherit` so the tone-aware text cascade still applies
625
+ (material sets a fixed `--color-text` which doesn't flip on dark).
626
+ - `box-shadow: none` because both materials want a flat surface; the
627
+ default control shadow fights glass especially.
628
+ - hover/active paint via `background-image` (a flat linear-gradient)
629
+ so the material's `background-color` is preserved underneath.
630
+ - dark-tone bumps the hairline to `--tint-400` so the rim reads on
631
+ dark backdrops (material's locked black-alpha border vanishes
632
+ against tone-dark patches). */
633
+ button[material],
634
+ segmented-control[material] {
635
+ color: inherit;
636
+ box-shadow: none;
637
+ }
638
+
639
+ button[material]:hover:not(:disabled) {
640
+ background-image: linear-gradient(var(--color-overlay-hover), var(--color-overlay-hover));
641
+ }
642
+
643
+ button[material]:active:not(:disabled) {
644
+ background-image: linear-gradient(var(--color-overlay-active), var(--color-overlay-active));
645
+ }
646
+
647
+ [tone="dark"] button[material],
648
+ [tone="dark"] segmented-control[material] {
649
+ border-color: var(--tint-400);
650
+ }
651
+
530
652
  button:disabled {
531
653
  background: var(--tint-300);
532
654
  border-color: transparent;
@@ -537,18 +659,26 @@ button:disabled {
537
659
  /* Default-variant button on a dark surface — drop the chromed light fill,
538
660
  pick up a brighter white-tint than inputs (15% vs 8%) to read as raised
539
661
  rather than recessed, and replace the ineffective black drop shadow
540
- with a top-edge inset highlight (macOS NSButton-on-dark convention). */
541
- [tone="dark"] button:not([variant]):not(:disabled) {
662
+ with a top-edge inset highlight (macOS NSButton-on-dark convention).
663
+ `:not([material])` opts the tone-driven default fill OUT when the
664
+ element has explicit `material` — material owns the surface in that
665
+ case, regardless of tone. */
666
+ :where([tone="dark"]) button:not([variant]):not([material]):not(:disabled),
667
+ :where([tone="dark"]) button[variant="default"]:not([material]):not(:disabled),
668
+ :where([tone="dark"]) segmented-control:not([variant]):not([material]),
669
+ :where([tone="dark"]) segmented-control[variant="default"]:not([material]) {
542
670
  background: var(--tint-200);
543
671
  border-color: var(--tint-300);
544
672
  box-shadow: inset 0 0.5px 0 var(--tint-300);
545
673
  }
546
674
 
547
- [tone="dark"] button:not([variant]):hover:not(:disabled) {
675
+ :where([tone="dark"]) button:not([variant]):hover:not(:disabled),
676
+ :where([tone="dark"]) button[variant="default"]:hover:not(:disabled) {
548
677
  background: var(--tint-300);
549
678
  }
550
679
 
551
- [tone="dark"] button:not([variant]):active:not(:disabled) {
680
+ :where([tone="dark"]) button:not([variant]):active:not(:disabled),
681
+ :where([tone="dark"]) button[variant="default"]:active:not(:disabled) {
552
682
  background: var(--tint-400);
553
683
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.20);
554
684
  }
@@ -558,39 +688,45 @@ button:disabled {
558
688
  one (e.g. a paper popover anchored inside dark chrome). Same
559
689
  specificity as the dark-tone rules above; source order makes the
560
690
  closer light-tone scope win for slotted descendants. */
561
- [tone="light"] button:not([variant]):not(:disabled) {
691
+ :where([tone="light"]) button:not([variant]):not([material]):not(:disabled),
692
+ :where([tone="light"]) button[variant="default"]:not([material]):not(:disabled),
693
+ :where([tone="light"]) segmented-control:not([variant]):not([material]),
694
+ :where([tone="light"]) segmented-control[variant="default"]:not([material]) {
562
695
  background: var(--color-surface);
563
696
  border-color: var(--tint-200);
564
697
  box-shadow: var(--shadow-control);
565
698
  }
566
699
 
567
- [tone="light"] button:not([variant]):hover:not(:disabled) {
700
+ :where([tone="light"]) button:not([variant]):hover:not(:disabled) {
568
701
  background: color-mix(in srgb, var(--color-surface) 96%, black);
569
702
  }
570
703
 
571
- [tone="light"] button:not([variant]):active:not(:disabled) {
704
+ :where([tone="light"]) button:not([variant]):active:not(:disabled) {
572
705
  background: color-mix(in srgb, var(--color-surface) 88%, black);
573
706
  box-shadow: inset 0 1px 2px rgb(0 0 0 / 0.08);
574
707
  }
575
708
 
576
- button[size="sm"] {
577
- min-height: 24px;
578
- padding: var(--space-2) var(--space-8);
709
+ /* Size opt-in: `size="sm"` swaps to the smaller control-height token.
710
+ Same applies to `<segmented-control size="sm">` so the host stretches
711
+ children correctly, and to `<button size="sm" icon-only>` so the
712
+ square footprint stays at the smaller control unit. The default
713
+ (md, 28px) is set on the base `<button>` rule above; only sm needs
714
+ an explicit override. */
715
+ button[size="sm"],
716
+ segmented-control[size="sm"] {
717
+ height: var(--control-height-sm);
579
718
  }
580
719
 
581
- button[size="md"] {
582
- min-height: 32px;
583
- padding: var(--space-6) var(--space-12);
720
+ button[size="sm"][icon-only] {
721
+ width: var(--control-height-sm);
584
722
  }
585
723
 
586
- /* Square icon-only buttons. Sizing only chrome is determined by the
587
- variant (default = chromed pill, ghost = chromeless overlay). The
588
- square footprint and zero padding stop a 24px SVG from being padded
589
- out by the text-button defaults. */
724
+ /* Square icon-only buttons. Width = control height so the footprint is
725
+ square at the same vertical size as a text-button sibling. Chrome is
726
+ determined by the variant (default = chromed pill, ghost = chromeless
727
+ overlay). */
590
728
  button[icon-only] {
591
- width: 24px;
592
- height: 24px;
593
- min-height: 0;
729
+ width: var(--control-height-md);
594
730
  padding: 0;
595
731
  }
596
732