@putdotio/design 3.1.0 → 3.2.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.
- package/DESIGN.md +40 -1
- package/README.md +4 -1
- package/package.json +6 -6
- package/system/README.md +3 -1
- package/system/components.css +156 -1
package/DESIGN.md
CHANGED
|
@@ -128,7 +128,7 @@ web-based TV app applies this system restrained to the product's generic
|
|
|
128
128
|
list-first 10-foot interface, which every TV surface — including the native
|
|
129
129
|
ones, within their platform conventions — treats as the shared family look.
|
|
130
130
|
|
|
131
|
-
Buttons have three tiers on one axis: emphasis. **Primary** is the brand fill — `--button-primary-bg` at rest, `--button-primary-bg-hover` on hover so the CTA never feels inert, label `--button-primary-fg`. Yellow is reserved for primary commands, and an action group carries at most one. **Secondary** is a quiet fill: `--button-default-bg` (aliased as `--button-secondary-bg`) plus a border at `--border`, with hover stepping both (`--button-default-bg-hover`, `--border-hover`). Because it fills, it reads to a hard edge exactly like the primary, so the two compare safely side by side. **Ghost** is text only — no fill, no border. `--button-ghost-fg` at rest; hover paints `--button-ghost-bg-hover` behind the label and lifts it to `--button-ghost-fg-hover`. The label lift is mandatory: the faint fill is never the only hover cue. Disabled drops the colour to `--solid` at full opacity rather than fading the box. Use ghost for anything quieter than a filled secondary — a header `
|
|
131
|
+
Buttons have three tiers on one axis: emphasis. **Primary** is the brand fill — `--button-primary-bg` at rest, `--button-primary-bg-hover` on hover so the CTA never feels inert, label `--button-primary-fg`. Yellow is reserved for primary commands, and an action group carries at most one. **Secondary** is a quiet fill: `--button-default-bg` (aliased as `--button-secondary-bg`) plus a border at `--border`, with hover stepping both (`--button-default-bg-hover`, `--border-hover`). Because it fills, it reads to a hard edge exactly like the primary, so the two compare safely side by side. **Ghost** is text only — no fill, no border. `--button-ghost-fg` at rest; hover paints `--button-ghost-bg-hover` behind the label and lifts it to `--button-ghost-fg-hover`. The label lift is mandatory: the faint fill is never the only hover cue. Disabled drops the colour to `--solid` at full opacity rather than fading the box. Use ghost for anything quieter than a filled secondary — a header `Sign in`, toolbar text actions, `Cancel` beside a filled confirm. An icon-only ghost is the quiet icon button, on the same tokens.
|
|
132
132
|
|
|
133
133
|
Outline is not a tier. The system ships no transparent-background bordered button, and consumers must not hand-roll one. The pairing rule behind that: never place a border-only button in the same action group as a solid fill. At identical box heights the outlined shape reads smaller, because a solid resolves to a hard edge while a low-contrast hairline resolves to a soft one, and the eye compares the two extents differently. It is worst on dark surfaces, where a 15%-white hairline is barely visible at all. If a bordered treatment must sit near a fill, it must itself fill — the secondary tier — and carry its border at `--border`-step contrast, never a sub-20% hairline on transparent.
|
|
134
134
|
|
|
@@ -140,6 +140,45 @@ File rows are the core component: preserve raw names, keep metadata compact, and
|
|
|
140
140
|
|
|
141
141
|
Form fields use the shared `--field-*` aliases: `--field-bg` for the fill, `--field-border` for the resting border, and `--field-ring` for focus. Invalid fields opt in with `aria-invalid="true"` and use red border/text only; do not add red fill. Raised panels use the shared `--panel-*` aliases instead of auth- or screen-specific panel variables.
|
|
142
142
|
|
|
143
|
+
Browser Auth composition is fluid up to a `340px` column. It uses the standard
|
|
144
|
+
`36px` field recipe, a `30.33px` wordmark box (the `26px` ink height multiplied
|
|
145
|
+
by its `7/6` view-box ratio), and centered headings at `19px` / weight
|
|
146
|
+
`500`. The prompt and its action form a centered two-line footer. User-facing
|
|
147
|
+
actions say “Sign in” and “Sign up”, and an invalid credential response uses
|
|
148
|
+
“That username or password doesn't look right” with no terminal punctuation.
|
|
149
|
+
The shipped
|
|
150
|
+
`web-s06-auth` screen plus `web-p00c-auth-signin`, `web-p00d-auth-signup`, and
|
|
151
|
+
`web-p00e-auth-2fa` are the Tier-1 composition references for this contract.
|
|
152
|
+
|
|
153
|
+
Form feedback uses the exported `.form-callout` recipe. Its block form reads
|
|
154
|
+
the `--alert-*` families and selects `info`, `success`, or `error` with
|
|
155
|
+
`data-state`; `.inline` removes the surface and border while keeping the same
|
|
156
|
+
semantic text colour. Callouts remain messages, not containers for unrelated
|
|
157
|
+
form layout. Their parent uses `.form-group`, whose `6px` gap owns the spacing
|
|
158
|
+
between a control and its inline feedback.
|
|
159
|
+
|
|
160
|
+
One-time passwords use one semantic `.otp-input`, never six independent
|
|
161
|
+
inputs. Place it with two three-slot `.otp-group` elements, one
|
|
162
|
+
`.otp-separator`, and six visual `.otp-slot` elements. The input carries
|
|
163
|
+
`inputmode="numeric"`, `autocomplete="one-time-code"`, `maxlength="6"`, and
|
|
164
|
+
`aria-invalid`; script synchronizes slot text plus `filled` / `active` state.
|
|
165
|
+
Pointer interaction maps the clicked visual slot to the backing input's
|
|
166
|
+
selection; do not rely on the transparent input's native glyph hit testing.
|
|
167
|
+
Editing an invalid code clears its stale error and disables retry until all six
|
|
168
|
+
digits are present. Retrying enters the same locked verifying state as the
|
|
169
|
+
default form.
|
|
170
|
+
The root uses `data-state="verifying"` while submission is in flight and keeps
|
|
171
|
+
the entered digits visible; the backing input is disabled for the same period,
|
|
172
|
+
and the action label announces the transition through a polite live region.
|
|
173
|
+
Successful verification uses `data-state="success"`, retains the six digits in
|
|
174
|
+
a read-only input, announces automatic continuation with a `role="status"`
|
|
175
|
+
callout, and does not leave another action on screen. The consumer owns the
|
|
176
|
+
backend transition and navigation timing.
|
|
177
|
+
The reference omits recovery-code entry until its consumer owns a working
|
|
178
|
+
route. Do not expose a specimen-only recovery action.
|
|
179
|
+
Groups, not slots, own the responsive width, so the complete control contracts
|
|
180
|
+
to narrow Auth cards without horizontal scrolling.
|
|
181
|
+
|
|
143
182
|
TV components are list-first and focus-first. A TV app is still a file browser, not a poster wall. Roku, native Apple, Android, and web TV repos should consume the generic tokens and generate their own platform bindings.
|
|
144
183
|
|
|
145
184
|
TV surfaces are solid token colors — no translucent materials and no blur. Focus is expressed as a fill, never a lift: rows go transparent to `--component-bg-active`, and buttons step both their fill and their border. Navigation is a Home hub that pushes full screens onto a stack; there is no tab bar. Menus are centred modals, not popovers anchored to a trigger. These focus and material rules bind the web TV and Roku surfaces; a native TV binding follows its platform's own control conventions where the platform owns focus — Apple tvOS buttons use the system focus treatment (adopted 2026-08) while non-control TV surfaces stay solid everywhere.
|
package/README.md
CHANGED
|
@@ -33,10 +33,12 @@ npm install @putdotio/design
|
|
|
33
33
|
|
|
34
34
|
## Use
|
|
35
35
|
|
|
36
|
-
Import the generated
|
|
36
|
+
Import the generated tokens first. Tier-1 web surfaces also import the
|
|
37
|
+
component recipes:
|
|
37
38
|
|
|
38
39
|
```css
|
|
39
40
|
@import "@putdotio/design/css";
|
|
41
|
+
@import "@putdotio/design/components";
|
|
40
42
|
```
|
|
41
43
|
|
|
42
44
|
Component recipes should consume semantic aliases such as `--field-*`,
|
|
@@ -46,6 +48,7 @@ into palette tokens directly.
|
|
|
46
48
|
Package entrypoints:
|
|
47
49
|
|
|
48
50
|
- CSS custom properties: [`dist/css/tokens.css`](dist/css/tokens.css)
|
|
51
|
+
- Tier-1 component recipes: [`system/components.css`](system/components.css)
|
|
49
52
|
- DTCG token tree: [`dist/tokens.dtcg.json`](dist/tokens.dtcg.json)
|
|
50
53
|
- Flat token metadata: [`dist/tokens.flat.json`](dist/tokens.flat.json)
|
|
51
54
|
- Figma-safe subset: [`dist/figma/putio.tokens.json`](dist/figma/putio.tokens.json)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@putdotio/design",
|
|
3
|
-
"version": "3.1
|
|
3
|
+
"version": "3.2.1",
|
|
4
4
|
"description": "Public put.io design tokens, brand assets, and design-system guidance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -67,16 +67,16 @@
|
|
|
67
67
|
"scripts": {
|
|
68
68
|
"dev": "node scripts/serve-system.ts",
|
|
69
69
|
"tokens:build": "node scripts/build-tokens.ts",
|
|
70
|
-
"tokens:check": "node scripts/
|
|
70
|
+
"tokens:check": "node scripts/build-tokens.ts && node scripts/check-component-css.ts && node scripts/verify-design-system.ts",
|
|
71
71
|
"design:mirror": "node scripts/export-design-mirror.ts",
|
|
72
|
+
"test:config": "node --test tests/playwright-config.test.mjs",
|
|
72
73
|
"test:mirror": "node --test tests/export-design-mirror.test.mjs",
|
|
73
74
|
"typecheck:scripts": "tsc --noEmit --ignoreConfig --skipLibCheck --module ESNext --moduleResolution Bundler --target ES2022 --strict --types node scripts/*.ts playwright.config.ts tests/e2e/*.ts sst.config.ts infra/*.ts",
|
|
74
|
-
"smoke": "node scripts/smoke-static.ts",
|
|
75
75
|
"html:check": "html-validate \"system/**/*.html\"",
|
|
76
76
|
"skills:lint": "skillcheck lint",
|
|
77
77
|
"e2e": "playwright test",
|
|
78
|
-
"verify": "pnpm skills:lint && pnpm typecheck:scripts && pnpm tokens:check && pnpm
|
|
79
|
-
"verify:full": "pnpm verify && pnpm e2e && npm pack --dry-run",
|
|
78
|
+
"verify": "pnpm skills:lint && pnpm typecheck:scripts && pnpm tokens:check && pnpm html:check",
|
|
79
|
+
"verify:full": "pnpm verify && pnpm test:mirror && pnpm test:config && pnpm e2e && npm pack --dry-run",
|
|
80
80
|
"check": "pnpm verify",
|
|
81
81
|
"deploy:production": "sst deploy --stage production"
|
|
82
82
|
},
|
|
@@ -91,5 +91,5 @@
|
|
|
91
91
|
"typescript": "^7.0.2",
|
|
92
92
|
"yaml": "^2.9.0"
|
|
93
93
|
},
|
|
94
|
-
"packageManager": "pnpm@
|
|
94
|
+
"packageManager": "pnpm@12.0.0"
|
|
95
95
|
}
|
package/system/README.md
CHANGED
|
@@ -68,10 +68,12 @@ font-size is responsive; the canonical values live in
|
|
|
68
68
|
rule: CSS media queries can't read custom properties, so write the literal px
|
|
69
69
|
in `@media` and keep it in sync with the `--bp-*` token.
|
|
70
70
|
|
|
71
|
-
Web package consumers import the
|
|
71
|
+
Web package consumers import the generated tokens first. Tier-1 consumers also
|
|
72
|
+
import the component recipes:
|
|
72
73
|
|
|
73
74
|
```css
|
|
74
75
|
@import "@putdotio/design/css";
|
|
76
|
+
@import "@putdotio/design/components";
|
|
75
77
|
```
|
|
76
78
|
|
|
77
79
|
For **TV preview cards**, also load `tv.css` — it adds the `.tv`, `.scr`,
|
package/system/components.css
CHANGED
|
@@ -137,7 +137,7 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
|
|
|
137
137
|
hover paints the shared quiet fill and lifts the label a step
|
|
138
138
|
(--button-ghost-fg → --button-ghost-fg-hover — the fill is never the
|
|
139
139
|
only cue). For actions quieter than a filled secondary: a header
|
|
140
|
-
"
|
|
140
|
+
"Sign in", toolbar text actions, "Cancel" beside a filled confirm.
|
|
141
141
|
NEVER substitute an outline: a border-only box beside a solid fill
|
|
142
142
|
reads as a different size — see the pairing rule in DESIGN.md.
|
|
143
143
|
Disabled matches the quiet treatment: the colour drops to --solid,
|
|
@@ -357,6 +357,161 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
357
357
|
}
|
|
358
358
|
.form-group .label-row a:hover { color: var(--text); }
|
|
359
359
|
|
|
360
|
+
/* ===========================================================
|
|
361
|
+
Form feedback — one public recipe for block and inline status.
|
|
362
|
+
|
|
363
|
+
`data-state` is the tone: info (the default), success, or error.
|
|
364
|
+
Block callouts consume the dedicated --alert-* families. `.inline`
|
|
365
|
+
removes their chrome but deliberately keeps the same semantic text
|
|
366
|
+
colour, so a field error does not invent another red.
|
|
367
|
+
=========================================================== */
|
|
368
|
+
.form-callout,
|
|
369
|
+
.form-callout[data-state="info"] {
|
|
370
|
+
box-sizing: border-box;
|
|
371
|
+
width: 100%;
|
|
372
|
+
display: flex;
|
|
373
|
+
align-items: start;
|
|
374
|
+
gap: 8px;
|
|
375
|
+
padding: 8px 10px;
|
|
376
|
+
border: 1px solid var(--alert-info-border);
|
|
377
|
+
border-radius: var(--alert-radius);
|
|
378
|
+
background: var(--alert-info-bg);
|
|
379
|
+
color: var(--alert-info-body);
|
|
380
|
+
font-size: var(--fs-sm);
|
|
381
|
+
line-height: var(--lh-normal);
|
|
382
|
+
}
|
|
383
|
+
.form-callout[hidden] { display: none; }
|
|
384
|
+
.form-callout > :is(i, svg) {
|
|
385
|
+
flex-shrink: 0;
|
|
386
|
+
color: var(--alert-info-icon);
|
|
387
|
+
font-size: 14px;
|
|
388
|
+
line-height: var(--lh-normal);
|
|
389
|
+
}
|
|
390
|
+
.form-callout[data-state="success"] {
|
|
391
|
+
border-color: var(--alert-success-border);
|
|
392
|
+
background: var(--alert-success-bg);
|
|
393
|
+
color: var(--alert-success-body);
|
|
394
|
+
}
|
|
395
|
+
.form-callout[data-state="success"] > :is(i, svg) { color: var(--alert-success-icon); }
|
|
396
|
+
.form-callout[data-state="error"] {
|
|
397
|
+
border-color: var(--alert-danger-border);
|
|
398
|
+
background: var(--alert-danger-bg);
|
|
399
|
+
color: var(--alert-danger-body);
|
|
400
|
+
}
|
|
401
|
+
.form-callout[data-state="error"] > :is(i, svg) { color: var(--alert-danger-icon); }
|
|
402
|
+
.form-callout.inline {
|
|
403
|
+
padding: 0;
|
|
404
|
+
border: 0;
|
|
405
|
+
border-radius: 0;
|
|
406
|
+
background: transparent;
|
|
407
|
+
font-size: var(--fs-sm);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* ===========================================================
|
|
411
|
+
One-time password — one real input, six visual slots.
|
|
412
|
+
|
|
413
|
+
The transparent input spans the whole recipe so pointer, keyboard,
|
|
414
|
+
paste, password-manager and one-time-code interactions all reach the
|
|
415
|
+
same semantic control. Script only mirrors digits and the active slot;
|
|
416
|
+
invalid styling derives from aria-invalid on the input itself.
|
|
417
|
+
|
|
418
|
+
Each three-slot group is a fluid grid. Slots never publish a fixed
|
|
419
|
+
width, which keeps two groups plus their separator inside narrow cards.
|
|
420
|
+
=========================================================== */
|
|
421
|
+
.otp {
|
|
422
|
+
position: relative;
|
|
423
|
+
display: flex;
|
|
424
|
+
align-items: center;
|
|
425
|
+
gap: clamp(4px, 2vw, 8px);
|
|
426
|
+
width: 100%;
|
|
427
|
+
min-width: 0;
|
|
428
|
+
}
|
|
429
|
+
.otp-input {
|
|
430
|
+
position: absolute;
|
|
431
|
+
inset: 0;
|
|
432
|
+
z-index: 1;
|
|
433
|
+
width: 100%;
|
|
434
|
+
height: 100%;
|
|
435
|
+
box-sizing: border-box;
|
|
436
|
+
margin: 0;
|
|
437
|
+
padding: 0;
|
|
438
|
+
border: 0;
|
|
439
|
+
opacity: 0;
|
|
440
|
+
cursor: text;
|
|
441
|
+
}
|
|
442
|
+
.otp-group {
|
|
443
|
+
display: grid;
|
|
444
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
445
|
+
gap: clamp(4px, 2vw, 8px);
|
|
446
|
+
flex: 1 1 0;
|
|
447
|
+
min-width: 0;
|
|
448
|
+
}
|
|
449
|
+
.otp-slot {
|
|
450
|
+
box-sizing: border-box;
|
|
451
|
+
min-width: 0;
|
|
452
|
+
height: 52px;
|
|
453
|
+
display: grid;
|
|
454
|
+
place-items: center;
|
|
455
|
+
border: 1px solid var(--field-border);
|
|
456
|
+
border-radius: var(--field-radius);
|
|
457
|
+
background: var(--field-bg);
|
|
458
|
+
color: var(--field-text);
|
|
459
|
+
font-family: var(--font-mono);
|
|
460
|
+
font-size: 22px;
|
|
461
|
+
font-variant-numeric: tabular-nums;
|
|
462
|
+
line-height: 1;
|
|
463
|
+
pointer-events: none;
|
|
464
|
+
transition:
|
|
465
|
+
border-color var(--dur-fast) var(--ease-out),
|
|
466
|
+
box-shadow var(--dur-fast) var(--ease-out),
|
|
467
|
+
background var(--dur-fast) var(--ease-out),
|
|
468
|
+
color var(--dur-fast) var(--ease-out);
|
|
469
|
+
}
|
|
470
|
+
.otp-slot[data-state="filled"] { border-color: var(--field-border-hover); }
|
|
471
|
+
.otp:focus-within .otp-slot[data-state="active"] {
|
|
472
|
+
border-color: var(--field-border-focus);
|
|
473
|
+
box-shadow: var(--field-ring);
|
|
474
|
+
}
|
|
475
|
+
.otp-separator {
|
|
476
|
+
flex: 0 0 auto;
|
|
477
|
+
padding: 0;
|
|
478
|
+
color: var(--text-secondary);
|
|
479
|
+
font-size: 18px;
|
|
480
|
+
line-height: 1;
|
|
481
|
+
pointer-events: none;
|
|
482
|
+
}
|
|
483
|
+
.otp:has(.otp-input[aria-invalid="true"]) .otp-slot {
|
|
484
|
+
border-color: var(--red-border);
|
|
485
|
+
color: var(--red-text-secondary);
|
|
486
|
+
}
|
|
487
|
+
.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);
|
|
490
|
+
}
|
|
491
|
+
.otp[data-state="success"] .otp-slot {
|
|
492
|
+
border-color: var(--green-border);
|
|
493
|
+
color: var(--green-text-secondary);
|
|
494
|
+
}
|
|
495
|
+
.otp[data-state="success"]:focus-within .otp-slot[data-state="active"] {
|
|
496
|
+
border-color: var(--green-border-hover);
|
|
497
|
+
box-shadow: 0 0 0 1px var(--green-border-hover);
|
|
498
|
+
}
|
|
499
|
+
.otp[data-state="verifying"] .otp-input,
|
|
500
|
+
.otp[aria-disabled="true"] .otp-input,
|
|
501
|
+
.otp-input:disabled { pointer-events: none; }
|
|
502
|
+
.otp[data-state="verifying"] .otp-slot,
|
|
503
|
+
.otp[data-state="verifying"] .otp-slot[data-state="active"],
|
|
504
|
+
.otp[aria-disabled="true"] .otp-slot,
|
|
505
|
+
.otp[aria-disabled="true"] .otp-slot[data-state="active"],
|
|
506
|
+
.otp:has(.otp-input:disabled) .otp-slot,
|
|
507
|
+
.otp:has(.otp-input:disabled) .otp-slot[data-state="active"] {
|
|
508
|
+
border-color: var(--field-border);
|
|
509
|
+
background: var(--field-bg-disabled);
|
|
510
|
+
color: var(--text-secondary);
|
|
511
|
+
box-shadow: none;
|
|
512
|
+
cursor: not-allowed;
|
|
513
|
+
}
|
|
514
|
+
|
|
360
515
|
/* Floating-menu surface — one recipe for dropdowns, context menus,
|
|
361
516
|
submenus, and on-player popovers. Cards keep their item styles local.
|
|
362
517
|
Consumed by web-c09-menu.html and web-p06-player.html — it looks unused
|