@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.
- package/DESIGN.md +20 -8
- package/dist/css/tokens.css +23 -21
- package/dist/figma/putio.tokens.json +7 -3
- package/dist/tokens.d.ts +1 -1
- package/dist/tokens.dtcg.json +44 -20
- package/dist/tokens.flat.json +57 -41
- package/dist/tokens.js +57 -41
- package/package.json +1 -1
- package/system/README.md +4 -6
- package/system/components.css +154 -16
- package/tokens/components.tokens.json +18 -18
- package/tokens/primitives.tokens.json +26 -2
package/system/components.css
CHANGED
|
@@ -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
|
-
/*
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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.
|
|
215
|
-
|
|
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 (= --
|
|
256
|
-
border → --field-border (= --
|
|
257
|
-
border:hover → --field-border-hover
|
|
258
|
-
focus
|
|
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
|
-
|
|
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-
|
|
311
|
-
box-shadow: 0 0 0 1px var(--red-
|
|
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-
|
|
489
|
-
box-shadow: 0 0 0 1px var(--red-
|
|
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": "{
|
|
832
|
-
"$description": "Form-control fill. Fields
|
|
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": "{
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
928
|
-
"$description": "Focused form-control border
|
|
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.
|
|
940
|
-
"$description": "Dark-mode focused form-control border
|
|
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.
|
|
1012
|
-
"$description": "Quiet field focus ring
|
|
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.
|
|
1024
|
-
"$description": "Dark-mode quiet field focus ring.
|
|
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": "{
|
|
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": "{
|
|
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
|
|
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": "
|
|
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",
|