@putdotio/design 3.7.0 → 3.9.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
@@ -251,11 +265,12 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
251
265
  now they all inherit from here. To override per-screen, change
252
266
  geometry (height/padding) — not the surface tokens.
253
267
 
254
- Surface contract:
255
- bg → --field-bg (= --app-bg)
256
- border → --field-border (= --border)
257
- border:hover → --field-border-hover
258
- focus ring → --field-ring (1px border-hover, NOT yellow)
268
+ Surface contract (filled, so a field reads on any surface):
269
+ bg → --field-bg (= --bg-secondary)
270
+ border → --field-border (= --line)
271
+ border:hover → --field-border-hover (= --border-hover)
272
+ border:focus → --field-border-focus (= --solid-hover, ≥3:1 on fill, page, panel)
273
+ focus ring → --field-ring (1px in the focus colour, NOT yellow)
259
274
  placeholder → --field-placeholder
260
275
  disabled bg → --field-bg-disabled
261
276
 
@@ -298,17 +313,20 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
298
313
  }
299
314
  /* Invalid — border + text only, no surface fill.
300
315
  Step convention: rest = step 7 (--red-border),
301
- focus/hover = step 8 (--red-border-hover). */
316
+ hover = step 8 (--red-border-hover), focus = step 9 (--red-solid),
317
+ which holds 3:1 against the field fill.
318
+ The border fades in over --dur-reveal, in step with .form-reveal. */
302
319
  .field[aria-invalid="true"] {
303
320
  border-color: var(--red-border);
321
+ transition-duration: var(--dur-reveal);
304
322
  }
305
323
  .field[aria-invalid="true"]:hover {
306
324
  border-color: var(--red-border-hover);
307
325
  }
308
326
  .field[aria-invalid="true"]:focus,
309
327
  .field[aria-invalid="true"]:focus-visible {
310
- border-color: var(--red-border-hover);
311
- box-shadow: 0 0 0 1px var(--red-border-hover);
328
+ border-color: var(--red-solid);
329
+ box-shadow: 0 0 0 1px var(--red-solid);
312
330
  }
313
331
  /* Loading / async-validating — suppress focus ring so the spinner
314
332
  reads as the dominant signal. */
@@ -407,6 +425,38 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
407
425
  font-size: var(--fs-sm);
408
426
  }
409
427
 
428
+ /* ===========================================================
429
+ Form reveal — eases a field or form error in without a layout jump.
430
+
431
+ Every field error uses it: under its field inside .form-group, or
432
+ as a form's one message above the submit button. The row animates
433
+ its height through grid-template-rows (0fr → 1fr) with opacity, so
434
+ whatever sits below slides instead of jumping. Markup:
435
+ .form-reveal > div > .form-callout.inline[data-state="error"]; the
436
+ inner div clips, so spacing on the message collapses with it. Keep
437
+ the message (role="alert") mounted and rendered, so screen readers
438
+ hear new text: set its text and data-state="open" together; on
439
+ close, set data-state="closed" and empty the message once the row
440
+ has collapsed (transitionend), so it does not snap and a closed row
441
+ says nothing. The token base collapses the transition under
442
+ prefers-reduced-motion.
443
+ =========================================================== */
444
+ .form-reveal {
445
+ display: grid;
446
+ grid-template-rows: 0fr;
447
+ opacity: 0;
448
+ transition:
449
+ grid-template-rows var(--dur-reveal) var(--ease-out),
450
+ opacity var(--dur-reveal) var(--ease-out);
451
+ }
452
+ .form-reveal > * { min-height: 0; overflow: hidden; }
453
+ .form-reveal .form-callout { margin: 0; }
454
+ .form-reveal[data-state="open"] { grid-template-rows: 1fr; opacity: 1; }
455
+ /* Inside a .form-group the row absorbs the group gap, so a closed row
456
+ adds no space and an open one sits the usual 6px under its field. */
457
+ .form-group > .form-reveal { margin-top: -6px; }
458
+ .form-group > .form-reveal .form-callout { padding-top: 6px; }
459
+
410
460
  /* ===========================================================
411
461
  One-time password — one real input, six visual slots.
412
462
 
@@ -483,10 +533,11 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
483
533
  .otp:has(.otp-input[aria-invalid="true"]) .otp-slot {
484
534
  border-color: var(--red-border);
485
535
  color: var(--red-text-secondary);
536
+ transition-duration: var(--dur-reveal);
486
537
  }
487
538
  .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);
539
+ border-color: var(--red-solid);
540
+ box-shadow: 0 0 0 1px var(--red-solid);
490
541
  }
491
542
  .otp[data-state="success"] .otp-slot {
492
543
  border-color: var(--green-border);
@@ -512,6 +563,93 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
512
563
  cursor: not-allowed;
513
564
  }
514
565
 
566
+ /* ===========================================================
567
+ Auth page — sign in, sign up, password reset, two-factor.
568
+
569
+ Card-less: the form sits on the page background. Logo top-left
570
+ in .auth-header, one left-aligned 340px column centred in
571
+ .auth-main, a quiet link row in .auth-footer. Controls are 44px;
572
+ the title is the only heading. Under 640px the gutters drop to 24px so the logo lines
573
+ up with the column.
574
+ =========================================================== */
575
+ .auth-page {
576
+ box-sizing: border-box;
577
+ min-height: 100dvh;
578
+ display: flex;
579
+ flex-direction: column;
580
+ background: var(--bg);
581
+ color: var(--text);
582
+ }
583
+ .auth-header {
584
+ display: flex;
585
+ align-items: center;
586
+ justify-content: space-between;
587
+ gap: 16px;
588
+ padding: 28px 32px;
589
+ }
590
+ .auth-logo { display: block; height: 30.33px; width: auto; }
591
+ .auth-main {
592
+ flex: 1 0 auto;
593
+ display: flex;
594
+ align-items: center;
595
+ justify-content: center;
596
+ padding: 24px 24px 48px;
597
+ }
598
+ .auth-column {
599
+ width: 100%;
600
+ max-width: 340px;
601
+ display: flex;
602
+ flex-direction: column;
603
+ }
604
+ .auth-title {
605
+ margin: 0 0 28px;
606
+ font-size: 22px;
607
+ line-height: 30px;
608
+ font-weight: var(--fw-medium);
609
+ letter-spacing: -0.01em;
610
+ color: var(--text);
611
+ }
612
+ .auth-column .form-stack { gap: 16px; }
613
+ .auth-column .label {
614
+ font-size: var(--fs-sm);
615
+ font-weight: var(--fw-regular);
616
+ color: var(--text-secondary);
617
+ }
618
+ .auth-column .field { height: 44px; }
619
+ .auth-column form > .form-reveal .form-callout { padding-top: 12px; font-size: var(--fs-base); }
620
+ .auth-column .btn-block { height: 44px; }
621
+ .auth-column form > .form-reveal + .btn-block { margin-top: 16px; }
622
+ .auth-links {
623
+ margin: 24px 0 0;
624
+ display: flex;
625
+ flex-wrap: wrap;
626
+ align-items: baseline;
627
+ justify-content: space-between;
628
+ gap: 8px 16px;
629
+ color: var(--text-secondary);
630
+ font-size: var(--fs-base);
631
+ }
632
+ /* A prompt's link reads as text ("New to put.io? Sign up"); a bare
633
+ action link ("Use a recovery code") stays muted. */
634
+ .auth-links p { margin: 0; }
635
+ .auth-links a { color: var(--text); text-underline-offset: 3px; }
636
+ .auth-links > a { color: var(--text-secondary); text-decoration: none; }
637
+ .auth-links > a:hover { color: var(--text); }
638
+ .auth-footer {
639
+ display: flex;
640
+ flex-wrap: wrap;
641
+ justify-content: center;
642
+ gap: 8px 16px;
643
+ padding: 24px 32px 28px;
644
+ font-size: var(--fs-sm);
645
+ }
646
+ .auth-footer a { color: var(--text-secondary); text-decoration: none; }
647
+ .auth-footer a:hover { color: var(--text); }
648
+ @media (max-width: 640px) {
649
+ .auth-header { padding: 24px; }
650
+ .auth-footer { padding: 24px; }
651
+ }
652
+
515
653
  /* Floating-menu surface — one recipe for dropdowns, context menus,
516
654
  submenus, and on-player popovers. Cards keep their item styles local.
517
655
  Consumed by web-c09-menu.html and web-p06-player.html — it looks unused
@@ -828,8 +828,8 @@
828
828
  "field": {
829
829
  "background": {
830
830
  "$type": "color",
831
- "$value": "{surface.light.appBg}",
832
- "$description": "Form-control fill. Fields stay on the app background surface, even inside a raised panel.",
831
+ "$value": "{color.neutral.light.bgSecondary}",
832
+ "$description": "Form-control fill. Fields are filled so they read on the page background as well as inside panels.",
833
833
  "$extensions": {
834
834
  "putio": {
835
835
  "cssName": "field-bg",
@@ -840,7 +840,7 @@
840
840
  },
841
841
  "backgroundDark": {
842
842
  "$type": "color",
843
- "$value": "{surface.dark.appBg}",
843
+ "$value": "{color.neutral.dark.bgSecondary}",
844
844
  "$description": "Dark-mode form-control fill.",
845
845
  "$extensions": {
846
846
  "putio": {
@@ -852,8 +852,8 @@
852
852
  },
853
853
  "backgroundDisabled": {
854
854
  "$type": "color",
855
- "$value": "{color.neutral.light.bgSecondary}",
856
- "$description": "Disabled form-control fill.",
855
+ "$value": "{color.neutral.light.componentBg}",
856
+ "$description": "Disabled form-control fill, a step off the resting fill.",
857
857
  "$extensions": {
858
858
  "putio": {
859
859
  "cssName": "field-bg-disabled",
@@ -864,7 +864,7 @@
864
864
  },
865
865
  "backgroundDisabledDark": {
866
866
  "$type": "color",
867
- "$value": "{color.neutral.dark.bgSecondary}",
867
+ "$value": "{color.neutral.dark.componentBg}",
868
868
  "$description": "Dark-mode disabled form-control fill.",
869
869
  "$extensions": {
870
870
  "putio": {
@@ -876,7 +876,7 @@
876
876
  },
877
877
  "border": {
878
878
  "$type": "color",
879
- "$value": "{color.neutral.light.border}",
879
+ "$value": "{color.neutral.light.line}",
880
880
  "$description": "Resting form-control border.",
881
881
  "$extensions": {
882
882
  "putio": {
@@ -888,7 +888,7 @@
888
888
  },
889
889
  "borderDark": {
890
890
  "$type": "color",
891
- "$value": "{color.neutral.dark.border}",
891
+ "$value": "{color.neutral.dark.line}",
892
892
  "$description": "Dark-mode resting form-control border.",
893
893
  "$extensions": {
894
894
  "putio": {
@@ -924,8 +924,8 @@
924
924
  },
925
925
  "borderFocus": {
926
926
  "$type": "color",
927
- "$value": "{color.neutral.light.borderHover}",
928
- "$description": "Focused form-control border. Focus matches hover and does not use the yellow focus shadow.",
927
+ "$value": "{color.neutral.light.solidHover}",
928
+ "$description": "Focused form-control border, a step past hover; at least 3:1 against the fill, the page, and panels. Does not use the yellow focus shadow.",
929
929
  "$extensions": {
930
930
  "putio": {
931
931
  "cssName": "field-border-focus",
@@ -936,8 +936,8 @@
936
936
  },
937
937
  "borderFocusDark": {
938
938
  "$type": "color",
939
- "$value": "{color.neutral.dark.borderHover}",
940
- "$description": "Dark-mode focused form-control border. Focus matches hover and does not use the yellow focus shadow.",
939
+ "$value": "{color.neutral.dark.solidHover}",
940
+ "$description": "Dark-mode focused form-control border, a step past hover; at least 3:1 against the fill, the page, and panels.",
941
941
  "$extensions": {
942
942
  "putio": {
943
943
  "cssName": "field-border-focus",
@@ -1008,8 +1008,8 @@
1008
1008
  },
1009
1009
  "ring": {
1010
1010
  "$type": "string",
1011
- "$value": "0 0 0 1px {color.neutral.light.borderHover}",
1012
- "$description": "Quiet field focus ring. Brand-yellow shadow focus is reserved for buttons, links, nav, and TV focus.",
1011
+ "$value": "0 0 0 1px {color.neutral.light.solidHover}",
1012
+ "$description": "Quiet field focus ring in the focus border colour. Buttons, links, and nav focus with --shadow-focus instead.",
1013
1013
  "$extensions": {
1014
1014
  "putio": {
1015
1015
  "cssName": "field-ring",
@@ -1020,8 +1020,8 @@
1020
1020
  },
1021
1021
  "ringDark": {
1022
1022
  "$type": "string",
1023
- "$value": "0 0 0 1px {color.neutral.dark.borderHover}",
1024
- "$description": "Dark-mode quiet field focus ring. Brand-yellow shadow focus is reserved for buttons, links, nav, and TV focus.",
1023
+ "$value": "0 0 0 1px {color.neutral.dark.solidHover}",
1024
+ "$description": "Dark-mode quiet field focus ring. Buttons, links, and nav focus with --shadow-focus instead.",
1025
1025
  "$extensions": {
1026
1026
  "putio": {
1027
1027
  "cssName": "field-ring",
@@ -1500,7 +1500,7 @@
1500
1500
  },
1501
1501
  "input": {
1502
1502
  "$type": "color",
1503
- "$value": "{color.neutral.light.border}",
1503
+ "$value": "{component.field.border}",
1504
1504
  "$extensions": {
1505
1505
  "putio": {
1506
1506
  "cssName": "input",
@@ -1511,7 +1511,7 @@
1511
1511
  },
1512
1512
  "inputDark": {
1513
1513
  "$type": "color",
1514
- "$value": "{color.neutral.dark.border}",
1514
+ "$value": "{component.field.borderDark}",
1515
1515
  "$extensions": {
1516
1516
  "putio": {
1517
1517
  "cssName": "input",
@@ -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",