@putdotio/design 3.6.3 → 3.8.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.
@@ -147,11 +147,24 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
147
147
  .btn-ghost:disabled { opacity: 1; color: var(--solid); background: transparent; }
148
148
 
149
149
  /* States */
150
- /* Disabled buttons freeze their resting fill at reduced opacity and ignore
151
- hover entirely — hover styles above are gated on :not(:disabled), so there
152
- is no need to (and we must not) reset background to `inherit` here, which
153
- would strip a disabled primary/danger button of its color on hover. */
150
+ /* Bare disabled buttons fade; hover styles above are gated on :not(:disabled).
151
+ Filled variants instead drop their fill to --solid at full opacity, so a
152
+ disabled "Verifying…" stays legible. A finished action keeps its success
153
+ fill with data-state="success" (2FA "Verified"). */
154
154
  button:disabled { opacity: 0.3; cursor: not-allowed; }
155
+ :is(.btn-primary, .btn-success, .btn-danger, .btn-info, .btn-invert):not(.btn-quiet):disabled {
156
+ opacity: 1;
157
+ background: var(--solid);
158
+ border-color: var(--solid);
159
+ color: var(--solid-foreground);
160
+ }
161
+ .btn-default:not(.btn-quiet):disabled { opacity: 1; color: var(--solid); }
162
+ .btn-success[data-state="success"]:disabled {
163
+ background: var(--button-success-bg);
164
+ border-color: var(--button-success-bg);
165
+ color: var(--button-success-fg);
166
+ }
167
+ /* An opaque double ring: a --bg gap, then --solid-hover, ≥3:1 on page and panels. */
155
168
  button:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
156
169
 
157
170
  /* Loading state — hides label, paints a CSS spinner over the button */
@@ -211,8 +224,9 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
211
224
 
212
225
  .card = focal, elevated component surface. Sits ON --bg,
213
226
  uses --component-bg so it reads as raised in dark
214
- and contained in light. Auth card, modal, settings
215
- card, anything the user's attention should go TO.
227
+ and contained in light. Modal, settings card,
228
+ anything the user's attention should go TO. Auth
229
+ pages take neither: see .auth-page.
216
230
 
217
231
  .panel = quiet inline reference container. Sits ON --bg,
218
232
  uses --bg-secondary so it stays recessive but
@@ -298,17 +312,20 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
298
312
  }
299
313
  /* Invalid — border + text only, no surface fill.
300
314
  Step convention: rest = step 7 (--red-border),
301
- focus/hover = step 8 (--red-border-hover). */
315
+ hover = step 8 (--red-border-hover), focus = step 9 (--red-solid),
316
+ which holds 3:1 against the field fill.
317
+ The border fades in over --dur-reveal, in step with .form-reveal. */
302
318
  .field[aria-invalid="true"] {
303
319
  border-color: var(--red-border);
320
+ transition-duration: var(--dur-reveal);
304
321
  }
305
322
  .field[aria-invalid="true"]:hover {
306
323
  border-color: var(--red-border-hover);
307
324
  }
308
325
  .field[aria-invalid="true"]:focus,
309
326
  .field[aria-invalid="true"]:focus-visible {
310
- border-color: var(--red-border-hover);
311
- box-shadow: 0 0 0 1px var(--red-border-hover);
327
+ border-color: var(--red-solid);
328
+ box-shadow: 0 0 0 1px var(--red-solid);
312
329
  }
313
330
  /* Loading / async-validating — suppress focus ring so the spinner
314
331
  reads as the dominant signal. */
@@ -407,6 +424,38 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
407
424
  font-size: var(--fs-sm);
408
425
  }
409
426
 
427
+ /* ===========================================================
428
+ Form reveal — eases a field or form error in without a layout jump.
429
+
430
+ Every field error uses it: under its field inside .form-group, or
431
+ as a form's one message above the submit button. The row animates
432
+ its height through grid-template-rows (0fr → 1fr) with opacity, so
433
+ whatever sits below slides instead of jumping. Markup:
434
+ .form-reveal > div > .form-callout.inline[data-state="error"]; the
435
+ inner div clips, so spacing on the message collapses with it. Keep
436
+ the message (role="alert") mounted and rendered, so screen readers
437
+ hear new text: set its text and data-state="open" together; on
438
+ close, set data-state="closed" and empty the message once the row
439
+ has collapsed (transitionend), so it does not snap and a closed row
440
+ says nothing. The token base collapses the transition under
441
+ prefers-reduced-motion.
442
+ =========================================================== */
443
+ .form-reveal {
444
+ display: grid;
445
+ grid-template-rows: 0fr;
446
+ opacity: 0;
447
+ transition:
448
+ grid-template-rows var(--dur-reveal) var(--ease-out),
449
+ opacity var(--dur-reveal) var(--ease-out);
450
+ }
451
+ .form-reveal > * { min-height: 0; overflow: hidden; }
452
+ .form-reveal .form-callout { margin: 0; }
453
+ .form-reveal[data-state="open"] { grid-template-rows: 1fr; opacity: 1; }
454
+ /* Inside a .form-group the row absorbs the group gap, so a closed row
455
+ adds no space and an open one sits the usual 6px under its field. */
456
+ .form-group > .form-reveal { margin-top: -6px; }
457
+ .form-group > .form-reveal .form-callout { padding-top: 6px; }
458
+
410
459
  /* ===========================================================
411
460
  One-time password — one real input, six visual slots.
412
461
 
@@ -483,10 +532,11 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
483
532
  .otp:has(.otp-input[aria-invalid="true"]) .otp-slot {
484
533
  border-color: var(--red-border);
485
534
  color: var(--red-text-secondary);
535
+ transition-duration: var(--dur-reveal);
486
536
  }
487
537
  .otp:has(.otp-input[aria-invalid="true"]):focus-within .otp-slot[data-state="active"] {
488
- border-color: var(--red-border-hover);
489
- box-shadow: 0 0 0 1px var(--red-border-hover);
538
+ border-color: var(--red-solid);
539
+ box-shadow: 0 0 0 1px var(--red-solid);
490
540
  }
491
541
  .otp[data-state="success"] .otp-slot {
492
542
  border-color: var(--green-border);
@@ -512,6 +562,100 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
512
562
  cursor: not-allowed;
513
563
  }
514
564
 
565
+ /* ===========================================================
566
+ Auth page — sign in, sign up, password reset, two-factor.
567
+
568
+ Card-less: the form sits on the page background. Logo top-left
569
+ in .auth-header, one left-aligned 340px column centred in
570
+ .auth-main, a quiet link row in .auth-footer. The page re-points
571
+ --field-bg / --field-border, so .field and .otp-slot render filled
572
+ without a variant. Controls are 44px; the title is the only
573
+ heading. Under 640px the gutters drop to 24px so the logo lines
574
+ up with the column.
575
+ =========================================================== */
576
+ .auth-page {
577
+ --field-bg: var(--bg-secondary);
578
+ --field-border: var(--line);
579
+ /* Focus a step past hover, so it holds 3:1 against the filled field. */
580
+ --field-border-focus: var(--solid-hover);
581
+ --field-ring: 0 0 0 1px var(--solid-hover);
582
+ box-sizing: border-box;
583
+ min-height: 100dvh;
584
+ display: flex;
585
+ flex-direction: column;
586
+ background: var(--bg);
587
+ color: var(--text);
588
+ }
589
+ .auth-header {
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: space-between;
593
+ gap: 16px;
594
+ padding: 28px 32px;
595
+ }
596
+ .auth-logo { display: block; height: 30.33px; width: auto; }
597
+ .auth-main {
598
+ flex: 1 0 auto;
599
+ display: flex;
600
+ align-items: center;
601
+ justify-content: center;
602
+ padding: 24px 24px 48px;
603
+ }
604
+ .auth-column {
605
+ width: 100%;
606
+ max-width: 340px;
607
+ display: flex;
608
+ flex-direction: column;
609
+ }
610
+ .auth-title {
611
+ margin: 0 0 28px;
612
+ font-size: 22px;
613
+ line-height: 30px;
614
+ font-weight: var(--fw-medium);
615
+ letter-spacing: -0.01em;
616
+ color: var(--text);
617
+ }
618
+ .auth-column .form-stack { gap: 16px; }
619
+ .auth-column .label {
620
+ font-size: var(--fs-sm);
621
+ font-weight: var(--fw-regular);
622
+ color: var(--text-secondary);
623
+ }
624
+ .auth-column .field { height: 44px; }
625
+ .auth-column form > .form-reveal .form-callout { padding-top: 12px; font-size: var(--fs-base); }
626
+ .auth-column .btn-block { height: 44px; }
627
+ .auth-column form > .form-reveal + .btn-block { margin-top: 16px; }
628
+ .auth-links {
629
+ margin: 24px 0 0;
630
+ display: flex;
631
+ flex-wrap: wrap;
632
+ align-items: baseline;
633
+ justify-content: space-between;
634
+ gap: 8px 16px;
635
+ color: var(--text-secondary);
636
+ font-size: var(--fs-base);
637
+ }
638
+ /* A prompt's link reads as text ("New to put.io? Sign up"); a bare
639
+ action link ("Use a recovery code") stays muted. */
640
+ .auth-links p { margin: 0; }
641
+ .auth-links a { color: var(--text); text-underline-offset: 3px; }
642
+ .auth-links > a { color: var(--text-secondary); text-decoration: none; }
643
+ .auth-links > a:hover { color: var(--text); }
644
+ .auth-footer {
645
+ display: flex;
646
+ flex-wrap: wrap;
647
+ justify-content: center;
648
+ gap: 8px 16px;
649
+ padding: 24px 32px 28px;
650
+ font-size: var(--fs-sm);
651
+ }
652
+ .auth-footer a { color: var(--text-secondary); text-decoration: none; }
653
+ .auth-footer a:hover { color: var(--text); }
654
+ @media (max-width: 640px) {
655
+ .auth-header { padding: 24px; }
656
+ .auth-footer { padding: 24px; }
657
+ }
658
+
515
659
  /* Floating-menu surface — one recipe for dropdowns, context menus,
516
660
  submenus, and on-player popovers. Cards keep their item styles local.
517
661
  Consumed by web-c09-menu.html and web-p06-player.html — it looks unused
@@ -643,154 +643,184 @@
643
643
  }
644
644
  },
645
645
  "input": {
646
+ "$description": "Deprecated shadcn-shaped aliases of component.field. shadcn reads only --input (component.alias.input); author against --field-*.",
646
647
  "background": {
647
648
  "$type": "color",
648
649
  "$value": "{component.field.background}",
650
+ "$description": "Deprecated alias. Use --field-bg (component.field.background).",
649
651
  "$extensions": {
650
652
  "putio": {
651
653
  "cssName": "input-bg",
652
- "mode": "light"
654
+ "mode": "light",
655
+ "deprecated": true
653
656
  }
654
657
  }
655
658
  },
656
659
  "backgroundDark": {
657
660
  "$type": "color",
658
661
  "$value": "{component.field.backgroundDark}",
662
+ "$description": "Deprecated alias. Use --field-bg (component.field.backgroundDark).",
659
663
  "$extensions": {
660
664
  "putio": {
661
665
  "cssName": "input-bg",
662
- "mode": "dark"
666
+ "mode": "dark",
667
+ "deprecated": true
663
668
  }
664
669
  }
665
670
  },
666
671
  "border": {
667
672
  "$type": "color",
668
673
  "$value": "{component.field.border}",
674
+ "$description": "Deprecated alias. Use --field-border (component.field.border).",
669
675
  "$extensions": {
670
676
  "putio": {
671
677
  "cssName": "input-border",
672
- "mode": "light"
678
+ "mode": "light",
679
+ "deprecated": true
673
680
  }
674
681
  }
675
682
  },
676
683
  "borderDark": {
677
684
  "$type": "color",
678
685
  "$value": "{component.field.borderDark}",
686
+ "$description": "Deprecated alias. Use --field-border (component.field.borderDark).",
679
687
  "$extensions": {
680
688
  "putio": {
681
689
  "cssName": "input-border",
682
- "mode": "dark"
690
+ "mode": "dark",
691
+ "deprecated": true
683
692
  }
684
693
  }
685
694
  },
686
695
  "radius": {
687
696
  "$type": "dimension",
688
697
  "$value": "{component.field.radius}",
698
+ "$description": "Deprecated alias. Use --field-radius (component.field.radius).",
689
699
  "$extensions": {
690
700
  "putio": {
691
701
  "cssName": "input-radius",
692
- "mode": "global"
702
+ "mode": "global",
703
+ "deprecated": true
693
704
  }
694
705
  }
695
706
  },
696
707
  "borderHover": {
697
708
  "$type": "color",
698
709
  "$value": "{component.field.borderHover}",
699
- "$description": "shadcn-friendly alias of --field-border-hover. Always aliases the field token so the two name systems cannot drift.",
710
+ "$description": "Deprecated alias. Use --field-border-hover (component.field.borderHover).",
700
711
  "$extensions": {
701
712
  "putio": {
702
713
  "cssName": "input-border-hover",
703
- "mode": "light"
714
+ "mode": "light",
715
+ "deprecated": true
704
716
  }
705
717
  }
706
718
  },
707
719
  "borderHoverDark": {
708
720
  "$type": "color",
709
721
  "$value": "{component.field.borderHoverDark}",
722
+ "$description": "Deprecated alias. Use --field-border-hover (component.field.borderHoverDark).",
710
723
  "$extensions": {
711
724
  "putio": {
712
725
  "cssName": "input-border-hover",
713
- "mode": "dark"
726
+ "mode": "dark",
727
+ "deprecated": true
714
728
  }
715
729
  }
716
730
  },
717
731
  "borderFocus": {
718
732
  "$type": "color",
719
733
  "$value": "{component.field.borderFocus}",
734
+ "$description": "Deprecated alias. Use --field-border-focus (component.field.borderFocus).",
720
735
  "$extensions": {
721
736
  "putio": {
722
737
  "cssName": "input-border-focus",
723
- "mode": "light"
738
+ "mode": "light",
739
+ "deprecated": true
724
740
  }
725
741
  }
726
742
  },
727
743
  "borderFocusDark": {
728
744
  "$type": "color",
729
745
  "$value": "{component.field.borderFocusDark}",
746
+ "$description": "Deprecated alias. Use --field-border-focus (component.field.borderFocusDark).",
730
747
  "$extensions": {
731
748
  "putio": {
732
749
  "cssName": "input-border-focus",
733
- "mode": "dark"
750
+ "mode": "dark",
751
+ "deprecated": true
734
752
  }
735
753
  }
736
754
  },
737
755
  "ring": {
738
756
  "$type": "string",
739
757
  "$value": "{component.field.ring}",
758
+ "$description": "Deprecated alias. Use --field-ring (component.field.ring).",
740
759
  "$extensions": {
741
760
  "putio": {
742
761
  "cssName": "input-ring",
743
- "mode": "light"
762
+ "mode": "light",
763
+ "deprecated": true
744
764
  }
745
765
  }
746
766
  },
747
767
  "ringDark": {
748
768
  "$type": "string",
749
769
  "$value": "{component.field.ringDark}",
770
+ "$description": "Deprecated alias. Use --field-ring (component.field.ringDark).",
750
771
  "$extensions": {
751
772
  "putio": {
752
773
  "cssName": "input-ring",
753
- "mode": "dark"
774
+ "mode": "dark",
775
+ "deprecated": true
754
776
  }
755
777
  }
756
778
  },
757
779
  "text": {
758
780
  "$type": "color",
759
781
  "$value": "{component.field.text}",
782
+ "$description": "Deprecated alias. Use --field-text (component.field.text).",
760
783
  "$extensions": {
761
784
  "putio": {
762
785
  "cssName": "input-text",
763
- "mode": "light"
786
+ "mode": "light",
787
+ "deprecated": true
764
788
  }
765
789
  }
766
790
  },
767
791
  "textDark": {
768
792
  "$type": "color",
769
793
  "$value": "{component.field.textDark}",
794
+ "$description": "Deprecated alias. Use --field-text (component.field.textDark).",
770
795
  "$extensions": {
771
796
  "putio": {
772
797
  "cssName": "input-text",
773
- "mode": "dark"
798
+ "mode": "dark",
799
+ "deprecated": true
774
800
  }
775
801
  }
776
802
  },
777
803
  "placeholder": {
778
804
  "$type": "color",
779
805
  "$value": "{component.field.placeholder}",
806
+ "$description": "Deprecated alias. Use --field-placeholder (component.field.placeholder).",
780
807
  "$extensions": {
781
808
  "putio": {
782
809
  "cssName": "input-placeholder",
783
- "mode": "light"
810
+ "mode": "light",
811
+ "deprecated": true
784
812
  }
785
813
  }
786
814
  },
787
815
  "placeholderDark": {
788
816
  "$type": "color",
789
817
  "$value": "{component.field.placeholderDark}",
818
+ "$description": "Deprecated alias. Use --field-placeholder (component.field.placeholderDark).",
790
819
  "$extensions": {
791
820
  "putio": {
792
821
  "cssName": "input-placeholder",
793
- "mode": "dark"
822
+ "mode": "dark",
823
+ "deprecated": true
794
824
  }
795
825
  }
796
826
  }
@@ -1135,22 +1165,26 @@
1135
1165
  "foregroundMuted": {
1136
1166
  "$type": "color",
1137
1167
  "$value": "{color.neutral.light.textSecondary}",
1168
+ "$description": "Deprecated alias. Use --text-secondary (color.neutral.light.textSecondary).",
1138
1169
  "$extensions": {
1139
1170
  "putio": {
1140
1171
  "cssName": "foreground-muted",
1141
1172
  "mode": "light",
1142
- "figma": false
1173
+ "figma": false,
1174
+ "deprecated": true
1143
1175
  }
1144
1176
  }
1145
1177
  },
1146
1178
  "foregroundMutedDark": {
1147
1179
  "$type": "color",
1148
1180
  "$value": "{color.neutral.dark.textSecondary}",
1181
+ "$description": "Deprecated alias. Use --text-secondary (color.neutral.dark.textSecondary).",
1149
1182
  "$extensions": {
1150
1183
  "putio": {
1151
1184
  "cssName": "foreground-muted",
1152
1185
  "mode": "dark",
1153
- "figma": false
1186
+ "figma": false,
1187
+ "deprecated": true
1154
1188
  }
1155
1189
  }
1156
1190
  },
@@ -29,7 +29,7 @@
29
29
  "tertiary": {
30
30
  "$type": "color",
31
31
  "$value": "{context.tv.text.secondary}",
32
- "$description": "Deprecated compatibility alias. Use context.tv.text.secondary.",
32
+ "$description": "Deprecated alias. Use --text-2 (context.tv.text.secondary).",
33
33
  "$extensions": {
34
34
  "putio": {
35
35
  "cssName": "text-3",
@@ -86,11 +86,24 @@
86
86
  },
87
87
  "focus": {
88
88
  "$type": "string",
89
- "$value": "0 0 0 3px {elevation.focusColor}",
89
+ "$value": "0 0 0 2px {color.neutral.light.bg}, 0 0 0 4px {color.neutral.light.solidHover}",
90
90
  "$extensions": {
91
91
  "putio": {
92
92
  "cssName": "shadow-focus",
93
- "mode": "global",
93
+ "mode": "light",
94
+ "figma": false
95
+ }
96
+ },
97
+ "$description": "Button, link, and nav focus: a 2px page-coloured gap, then a 2px opaque ring at least 3:1 against the page and panels."
98
+ },
99
+ "focusDark": {
100
+ "$type": "string",
101
+ "$value": "0 0 0 2px {color.neutral.dark.bg}, 0 0 0 4px {color.neutral.dark.solidHover}",
102
+ "$description": "Dark-mode button, link, and nav focus ring.",
103
+ "$extensions": {
104
+ "putio": {
105
+ "cssName": "shadow-focus",
106
+ "mode": "dark",
94
107
  "figma": false
95
108
  }
96
109
  }
@@ -613,6 +626,17 @@
613
626
  }
614
627
  }
615
628
  },
629
+ "reveal": {
630
+ "$type": "duration",
631
+ "$value": "180ms",
632
+ "$extensions": {
633
+ "putio": {
634
+ "cssName": "dur-reveal",
635
+ "mode": "global",
636
+ "figma": true
637
+ }
638
+ }
639
+ },
616
640
  "base": {
617
641
  "$type": "duration",
618
642
  "$value": "200ms",