@putdotio/design 3.0.0 → 3.2.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 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 `Log in`, toolbar text actions, `Cancel` beside a filled confirm. An icon-only ghost is the quiet icon button, on the same tokens.
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 left-aligned 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 CSS when a web surface needs put.io custom properties:
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.0.0",
3
+ "version": "3.2.0",
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/check-generated-tokens.ts && node scripts/check-component-css.ts && node scripts/check-doc-tokens.ts && node scripts/verify-design-system.ts",
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 test:mirror && pnpm html:check && pnpm smoke && sst version",
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@11.2.2"
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 package CSS export:
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`,
@@ -197,7 +199,7 @@ they describe the role exactly. shadcn-imported code prefers the short names.
197
199
  Base UI / headless primitives set
198
200
  `data-state="open|closed|checked|unchecked|active|completed|pending|loading"`
199
201
  on the DOM. Our components target those same attribute values in CSS — see the
200
- wizard step bar in `web-p00-form-layouts.html`
202
+ wizard step bar in `web-p00b-form-flows.html`
201
203
  (`.step[data-state="completed"]`) and the menu/table cards. Prefer `data-state`
202
204
  over inventing class flags (`.is-active`, `.on`, `.current`) so the same DOM
203
205
  contract works whether the controlling code is React, Vue, vanilla JS, or
@@ -208,8 +210,8 @@ hand-written HTML, and so devtools can read the component's state at a glance.
208
210
  shadcn marks parts of compound components with `data-slot="card-header"`,
209
211
  `data-slot="card-title"`, etc. We do the same on the folder picker
210
212
  (`data-slot="modal" / "modal-header" / "modal-body" / "modal-footer"`) and the
211
- auth card in `web-p00-form-layouts.html` (`data-slot="auth" / "auth-header"
212
- / "auth-body"`). This lets external CSS tweak one part without inventing
213
+ auth cards in `web-p00c-auth-signin.html` and `web-p00d-auth-signup.html`
214
+ (`data-slot="auth" / "auth-header" / "auth-body"`). This lets external CSS tweak one part without inventing
213
215
  nested class chains.
214
216
 
215
217
  ## Design variants
@@ -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
- "Log in", toolbar text actions, "Cancel" beside a filled confirm.
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