@putdotio/design 5.2.0 → 5.3.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 +8 -7
- package/dist/css/tokens.css +3 -1
- package/package.json +9 -6
- package/system/components.css +105 -58
- package/tokens/foundation.css +3 -1
package/DESIGN.md
CHANGED
|
@@ -78,7 +78,7 @@ Tier 1, 3, and 4 cards are Components (put.io owns the recipe); tier 2 cards are
|
|
|
78
78
|
|
|
79
79
|
## Colors
|
|
80
80
|
|
|
81
|
-
- Yellow `#FDCE45` (`--yellow-solid`) is the brand value in both modes: brand mark, primary actions, folder and file emphasis, progress
|
|
81
|
+
- Yellow `#FDCE45` (`--yellow-solid`) is the brand value in both modes: brand mark, primary actions, folder and file emphasis, progress. Hover is `--yellow-solid-hover`.
|
|
82
82
|
- Every yellow is one of these tokens, flat. Yellow text on light surfaces is `--yellow-text-secondary`.
|
|
83
83
|
- Dark is the default product mode; light serves landing pages, docs, and states that read better light.
|
|
84
84
|
- Red, green, and neutral come from their token scales.
|
|
@@ -90,7 +90,7 @@ Tier 1, 3, and 4 cards are Components (put.io owns the recipe); tier 2 cards are
|
|
|
90
90
|
- TV product UI has no mono face (a debug overlay may use the platform's system monospace): 10-foot numerics use GT America `tnum`. Platforms without OpenType feature selection (Roku: `Font` takes only `uri` and `size`) get proportional figures, so budget timecodes for reflow (`1` is 399/1000 em, `0` is 636).
|
|
91
91
|
- GT America is Latin only (523 codepoints, Western and Central European plus Turkish, no dingbats such as `✓` `▶` `★`). Symbols are icons. Text that can hold a filename keeps a fallback family; web falls back per glyph, while one-font-per-label platforms must ship a fallback or render `.notdef` boxes.
|
|
92
92
|
- The root is the browser default, `100%` (16px), so text follows the reader's setting. Components choose a text role, not a size step: `--fs-ui` (14px) for UI chrome (labels, buttons, nav, links, errors), `--fs-body` (16px) for body and long text, `--fs-caption` (12px) for captions, and `--fs-field` (16px) for typed text, which keeps iOS Safari from zooming on focus.
|
|
93
|
-
- The steps are rem on Tailwind's 16px scale: `--fs-xs` 12px, `--fs-sm` 14, `--fs-base` 16, `--fs-md` 18, `--fs-lg` 24, `--fs-xl` 32, `--fs-2xl` 48, `--fs-3xl` 64, `--fs-display` 96. Screen titles
|
|
93
|
+
- The steps are rem on Tailwind's 16px scale: `--fs-xs` 12px, `--fs-sm` 14, `--fs-base` 16, `--fs-md` 18, `--fs-lg` 24, `--fs-xl` 32, `--fs-2xl` 48, `--fs-3xl` 64, `--fs-display` 96. Screen titles take `--fs-title` (22px) on a 30px line with `--tracking-snug`. One-time-code digits take `--fs-title` in mono; the code separator is `--fs-md`.
|
|
94
94
|
|
|
95
95
|
## Layout & Spacing
|
|
96
96
|
|
|
@@ -101,7 +101,7 @@ Tier 1, 3, and 4 cards are Components (put.io owns the recipe); tier 2 cards are
|
|
|
101
101
|
|
|
102
102
|
## Elevation & Depth
|
|
103
103
|
|
|
104
|
-
Borders and soft shadows mark interaction: popovers, modals, menus, TV focus, drag and drop targets.
|
|
104
|
+
Borders and soft shadows mark interaction: popovers, modals, menus, TV focus, drag and drop targets. Nothing glows; TV focus is a fill.
|
|
105
105
|
|
|
106
106
|
## Shapes
|
|
107
107
|
|
|
@@ -116,7 +116,8 @@ Default radius `6px`; `4px` for tight controls, `8px` or `10px` for panels, pill
|
|
|
116
116
|
- **Primary:** `--button-primary-bg`, hover `--button-primary-bg-hover`, label `--button-primary-fg`. At most one per action group.
|
|
117
117
|
- **Secondary:** `--button-default-bg` plus a `--border` border; hover steps both (`--button-default-bg-hover`, `--border-hover`).
|
|
118
118
|
- **Ghost:** text only, `--button-ghost-fg`; hover adds `--button-ghost-bg-hover` and lifts the label to `--button-ghost-fg-hover`. Use it for anything quieter than secondary: header `Sign in`, toolbar actions, `Cancel` beside a confirm, icon-only buttons.
|
|
119
|
-
- **Disabled:**
|
|
119
|
+
- **Disabled:** `:disabled` and `aria-disabled="true"` read the same. Submit buttons are never disabled at rest; the submit validates. A filled button is disabled only while its action runs (with a spinner) or once it is done, so it keeps its own fill and label at full opacity; a finished 2FA “Verified” is a `.btn-success` with `data-state="success"`. Secondary and quiet buttons drop their text to `--solid` at full opacity.
|
|
120
|
+
- **Hover:** only under `(hover: hover) and (pointer: fine)`, so a tap never leaves a button lit.
|
|
120
121
|
- **Focus:** `--shadow-focus`, a 2px `--bg` gap then a 2px `--solid-hover` ring, at least 3:1 against the page and panels in both modes.
|
|
121
122
|
- A bordered button always fills (secondary). Border-only buttons read smaller beside solid fills and vanish on dark.
|
|
122
123
|
- Four sizes share one box per size across tiers: default 36px, `md` 32px, `sm` 28px, `xs` 24px. Read `--button-height*`, `--button-padding-x*`, `--button-label-size*`, `--button-tracking*`, `--button-icon-size`, `--button-gap`.
|
|
@@ -135,14 +136,14 @@ Default radius `6px`; `4px` for tight controls, `8px` or `10px` for panels, pill
|
|
|
135
136
|
|
|
136
137
|
### Auth
|
|
137
138
|
|
|
138
|
-
- Sign in, sign up, password reset, and two-factor pages have no card or panel: the form sits on `--bg`. Recipe: `.auth-page`, `.auth-header`, `.auth-main`, `.auth-column`, `.auth-title`, `.auth-note`, `.auth-actions`, `.auth-links`, `.auth-footer`.
|
|
139
|
+
- Sign in, sign up, password reset, and two-factor pages have no card or panel: the form sits on `--bg`. Recipe: `.auth-page`, `.auth-header`, `.auth-logo`, `.auth-main`, `.auth-column`, `.auth-title`, `.auth-note`, `.auth-actions`, `.auth-links`, `.auth-footer`.
|
|
139
140
|
- The logo sits top-left in the header, in a `30.33px` box (26px ink × 7/6). The top right stays empty by default.
|
|
140
141
|
- One left-aligned column, up to `340px`, is centred horizontally and optically centred vertically: the form's middle sits near 45% of the viewport height, never below the true middle.
|
|
141
142
|
- A quiet centred footer links Terms of Service, Privacy Policy, and Help in muted `--fs-caption`, separated by middots, 16px above the page bottom.
|
|
142
143
|
- Text is one size: labels, hints, `.auth-note` lines, errors, and the link row take `--fs-ui`. The footer and a label's own action take `--fs-caption`; fields take `--fs-field`.
|
|
143
144
|
- One title per screen: `--fs-title` (`22px`), medium, `--tracking-snug` (`-0.01em`). No subtitles, taglines, or marketing lines; copy stays short.
|
|
144
145
|
- Fields and buttons keep the standard 36px box: the filled field and a full-width default-size primary.
|
|
145
|
-
- Labels are small, muted, and above the field.
|
|
146
|
+
- Labels are small, regular, muted, and above the field.
|
|
146
147
|
- The primary button is full-width yellow. “Forgot password?” is the Password field's `.label-action`. Other secondary actions sit under the form in `.auth-links`: a bare link stays muted (“Use a recovery code”, “← Back to sign in”); a prompt's link (“New to put.io? Sign up”, “Wrong account? Sign out”) is `--text`, medium, and underlines only on hover. Two items split to the edges; a lone link or prompt centres.
|
|
147
148
|
- Buttons outside a form sit in `.auth-actions`, 16px below what precedes them; a description `.auth-note` right before them leaves 28px. A back link leads with Phosphor's arrow-left as inline SVG. A legal line under the submit button centres on it.
|
|
148
149
|
- Tab order runs fields (each followed by its label action), submit, then secondary links; DOM order follows visual order, with no positive `tabindex`. Two-factor runs the code, Verify, then its links. The reveal-password toggle takes `tabindex="-1"` and keeps its accessible name: keyboard-only users can't toggle visibility, screen-reader users still can in browse mode. No link leaves the tab order.
|
|
@@ -150,7 +151,7 @@ Default radius `6px`; `4px` for tight controls, `8px` or `10px` for panels, pill
|
|
|
150
151
|
- Copy: “Sign in”, “Sign up”, and “That username or password doesn't look right.”
|
|
151
152
|
- auth.put.io still ships the older card layout: drift.
|
|
152
153
|
- References: `web-s06-auth`, `web-p00c-auth-signin`, `web-p00d-auth-signup`, `web-p00e-auth-2fa`. Outside references: the TIDAL, Stripe Link, Cursor, Linear, and Vercel sign-in pages.
|
|
153
|
-
- One-time codes are one `.otp-input` (`inputmode="numeric"`, `autocomplete="one-time-code"`, `maxlength="6"`, `aria-invalid`) shown through two `.otp-group`s, one `.otp-separator`, and six `.otp-slot`s; script syncs slot text and `filled`/`active`, and maps a clicked slot to the input selection. The slots stretch the full column with mono digits. The label reads “Code from your authenticator app”, with no hint line.
|
|
154
|
+
- One-time codes are one `.otp-input` (`inputmode="numeric"`, `autocomplete="one-time-code"`, `maxlength="6"`, `aria-invalid`) inside an `.otp` root, shown through two `.otp-group`s, one `.otp-separator`, and six `.otp-slot`s; script syncs slot text and `filled`/`active`, and maps a clicked slot to the input selection. The slots stretch the full column with mono digits. The label reads “Code from your authenticator app”, with no hint line.
|
|
154
155
|
- Editing an invalid code closes the error; retry waits for six digits. `data-state="verifying"` disables the input, keeps the digits, and announces through a polite live region. `data-state="success"` keeps the digits read-only, announces continuation in a `role="status"` region, and shows no further action; the consumer owns navigation. The submit turns a disabled `.btn-success` with `data-state="success"`, relabelled “Verified”, announced through a visually hidden status region.
|
|
155
156
|
- “Use a recovery code” appears only once a consumer owns the route. Groups own the responsive width, so the control fits narrow columns without scrolling.
|
|
156
157
|
|
package/dist/css/tokens.css
CHANGED
|
@@ -521,6 +521,9 @@
|
|
|
521
521
|
}
|
|
522
522
|
|
|
523
523
|
/* -- Document base ------------------------------------------------- */
|
|
524
|
+
/* Native scrollbars, controls and autofill follow the mode. */
|
|
525
|
+
:root { color-scheme: light; }
|
|
526
|
+
.dark { color-scheme: dark; }
|
|
524
527
|
html {
|
|
525
528
|
background: var(--html-bg);
|
|
526
529
|
color: var(--text);
|
|
@@ -555,7 +558,6 @@ body {
|
|
|
555
558
|
.num, .tabular {
|
|
556
559
|
font-family: var(--font-mono);
|
|
557
560
|
font-variant-numeric: tabular-nums;
|
|
558
|
-
font-feature-settings: "tnum";
|
|
559
561
|
}
|
|
560
562
|
|
|
561
563
|
h1, .h1 {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@putdotio/design",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.3.0",
|
|
4
4
|
"description": "Public put.io design tokens, brand assets, and design-system guidance.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -71,27 +71,30 @@
|
|
|
71
71
|
"tokens:check": "node scripts/build-tokens.ts && node scripts/check-component-css.ts && node scripts/check-tailwind-theme.ts && node scripts/verify-design-system.ts",
|
|
72
72
|
"artifact": "node scripts/build-artifact.ts",
|
|
73
73
|
"test:config": "node --test tests/playwright-config.test.mjs",
|
|
74
|
-
"typecheck:scripts": "tsc --noEmit --ignoreConfig --skipLibCheck --module ESNext --moduleResolution Bundler --target ES2022 --strict --types node scripts/*.ts playwright.config.ts tests/e2e/*.ts
|
|
74
|
+
"typecheck:scripts": "tsc --noEmit --ignoreConfig --skipLibCheck --module ESNext --moduleResolution Bundler --target ES2022 --strict --types node scripts/*.ts playwright.config.ts tests/e2e/*.ts alchemy.run.ts",
|
|
75
75
|
"html:check": "html-validate \"system/**/*.html\"",
|
|
76
76
|
"markdown:check": "oxfmt --check \"**/*.md\"",
|
|
77
77
|
"e2e": "playwright test",
|
|
78
78
|
"verify": "pnpm typecheck:scripts && pnpm tokens:check && pnpm artifact && pnpm html:check && pnpm markdown:check",
|
|
79
79
|
"verify:full": "pnpm verify && pnpm test:config && pnpm e2e && npm pack --dry-run",
|
|
80
80
|
"check": "pnpm verify",
|
|
81
|
-
"deploy:production": "
|
|
81
|
+
"deploy:production": "alchemy deploy --stage production --yes --no-input"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
+
"@alchemy.run/aws": "2.0.0-beta.84",
|
|
84
85
|
"@axe-core/playwright": "^4.11.3",
|
|
86
|
+
"@effect/platform-node": "4.1.2",
|
|
85
87
|
"@phosphor-icons/web": "2.1.2",
|
|
86
88
|
"@playwright/test": "^1.60.0",
|
|
87
89
|
"@types/node": "^26.0.0",
|
|
90
|
+
"alchemy": "2.0.0-beta.84",
|
|
91
|
+
"effect": "4.1.2",
|
|
88
92
|
"html-validate": "^11.2.0",
|
|
89
|
-
"oxfmt": "0.
|
|
90
|
-
"sst": "^4.14.3",
|
|
93
|
+
"oxfmt": "0.75.0",
|
|
91
94
|
"style-dictionary": "^5.4.1",
|
|
92
95
|
"tailwindcss": "^4.3.3",
|
|
93
96
|
"typescript": "^7.0.2",
|
|
94
97
|
"yaml": "^2.9.0"
|
|
95
98
|
},
|
|
96
|
-
"packageManager": "pnpm@12.
|
|
99
|
+
"packageManager": "pnpm@12.16.1"
|
|
97
100
|
}
|
package/system/components.css
CHANGED
|
@@ -79,6 +79,7 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
|
|
|
79
79
|
.btn-icon { width: calc(var(--button-height) * var(--tw-fs, 1)) !important; padding: 0 !important; text-transform: none; letter-spacing: normal; }
|
|
80
80
|
.btn-icon.btn-md { width: calc(var(--button-height-md) * var(--tw-fs, 1)) !important; }
|
|
81
81
|
.btn-icon.btn-sm { width: calc(var(--button-height-sm) * var(--tw-fs, 1)) !important; }
|
|
82
|
+
.btn-icon.btn-xs { width: calc(var(--button-height-xs) * var(--tw-fs, 1)) !important; }
|
|
82
83
|
|
|
83
84
|
/* Layout-only modifier — full-width CTA */
|
|
84
85
|
.btn-block { width: 100%; }
|
|
@@ -98,18 +99,17 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
|
|
|
98
99
|
.btn-quiet.btn-danger, .btn-quiet.btn-info, .btn-quiet.btn-default {
|
|
99
100
|
background: transparent; border-color: transparent; color: var(--button-ghost-fg);
|
|
100
101
|
}
|
|
101
|
-
|
|
102
|
+
/* The hover fill and focus ring round off; an icon-only quiet button takes the square box. */
|
|
103
|
+
.btn-quiet { border-radius: var(--button-radius); }
|
|
104
|
+
.btn-quiet.btn-icon { height: calc(var(--button-height) * var(--tw-fs, 1)); }
|
|
102
105
|
.btn-quiet.btn-info { color: var(--text); }
|
|
103
106
|
/* --yellow-text-secondary, not --yellow-solid: solid is a fill step, and in
|
|
104
107
|
light mode the brand yellow as a glyph colour on a light surface is
|
|
105
108
|
unreadable. The text step carries a legible value in both modes. */
|
|
106
109
|
.btn-quiet.btn-primary, .btn-quiet.btn-warning { color: var(--yellow-text-secondary); }
|
|
107
|
-
.btn-quiet.btn-primary:hover:not(:disabled), .btn-quiet.btn-warning:hover:not(:disabled) { background: var(--yellow-component-bg-hover); border-color: transparent; color: var(--yellow-text-secondary); }
|
|
108
110
|
.btn-quiet.btn-success { color: var(--green-solid); }
|
|
109
|
-
.btn-quiet.btn-success:hover:not(:disabled) { background: var(--green-component-bg-hover); border-color: transparent; color: var(--green-solid); }
|
|
110
111
|
.btn-quiet.btn-danger { color: var(--red-solid); }
|
|
111
|
-
.btn-quiet
|
|
112
|
-
.btn-quiet:disabled { opacity: 1; color: var(--solid); background: transparent; }
|
|
112
|
+
.btn-quiet:is(:disabled, [aria-disabled="true"]) { opacity: 1; color: var(--solid); background: transparent; }
|
|
113
113
|
|
|
114
114
|
/* Variants — token-driven. Foregrounds read from --*-foreground
|
|
115
115
|
tokens in tokens.css so "what color on top" lives in one place.
|
|
@@ -121,17 +121,11 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
|
|
|
121
121
|
accidental low-contrast bug. Never use these variants at xs/sm — the
|
|
122
122
|
label drops below the large-text threshold. */
|
|
123
123
|
.btn-primary { background: var(--button-primary-bg); border-color: var(--button-primary-bg); color: var(--button-primary-fg); }
|
|
124
|
-
.btn-primary:hover:not(:disabled) { background: var(--button-primary-bg-hover); border-color: var(--button-primary-bg-hover); }
|
|
125
124
|
.btn-success { background: var(--button-success-bg); border-color: var(--button-success-bg); color: var(--button-success-fg); }
|
|
126
|
-
.btn-success:hover:not(:disabled) { background: var(--button-success-bg-hover); border-color: var(--button-success-bg-hover); }
|
|
127
125
|
.btn-danger { background: var(--button-danger-bg); border-color: var(--button-danger-bg); color: var(--button-danger-fg); }
|
|
128
|
-
.btn-danger:hover:not(:disabled) { background: var(--button-danger-bg-hover); border-color: var(--button-danger-bg-hover); }
|
|
129
126
|
.btn-info { background: var(--button-info-bg); border-color: var(--button-info-bg); color: var(--button-info-fg); }
|
|
130
|
-
.btn-info:hover:not(:disabled) { background: var(--button-info-bg-hover); border-color: var(--button-info-bg-hover); }
|
|
131
127
|
.btn-invert { background: var(--text); border-color: var(--border); color: var(--invert-foreground); }
|
|
132
|
-
.btn-invert:hover:not(:disabled) { border-color: var(--border-hover); }
|
|
133
128
|
.btn-default { background: var(--button-default-bg); border-color: var(--border); color: var(--button-default-fg); }
|
|
134
|
-
.btn-default:hover:not(:disabled) { background: var(--button-default-bg-hover); border-color: var(--border-hover); }
|
|
135
129
|
|
|
136
130
|
/* Ghost — the third tier: text-only. No fill, no border at rest;
|
|
137
131
|
hover paints the shared quiet fill and lifts the label a step
|
|
@@ -143,36 +137,55 @@ button i { font-size: var(--button-icon-size); line-height: 1; display: inline-f
|
|
|
143
137
|
Disabled matches the quiet treatment: the colour drops to --solid,
|
|
144
138
|
the box does not fade. */
|
|
145
139
|
.btn-ghost { background: var(--button-ghost-bg); border-color: transparent; color: var(--button-ghost-fg); }
|
|
146
|
-
.btn-ghost:
|
|
147
|
-
|
|
140
|
+
.btn-ghost:is(:disabled, [aria-disabled="true"]) { opacity: 1; color: var(--solid); background: transparent; }
|
|
141
|
+
|
|
142
|
+
/* Hover — pointer devices only, so a tapped button doesn't stay lit on touch.
|
|
143
|
+
Quiet comes after the filled variants, so a quiet info button keeps the
|
|
144
|
+
ghost hover. */
|
|
145
|
+
@media (hover: hover) and (pointer: fine) {
|
|
146
|
+
.btn-primary:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-primary-bg-hover); border-color: var(--button-primary-bg-hover); }
|
|
147
|
+
.btn-success:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-success-bg-hover); border-color: var(--button-success-bg-hover); }
|
|
148
|
+
.btn-danger:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-danger-bg-hover); border-color: var(--button-danger-bg-hover); }
|
|
149
|
+
.btn-info:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-info-bg-hover); border-color: var(--button-info-bg-hover); }
|
|
150
|
+
.btn-invert:hover:not(:disabled, [aria-disabled="true"]) { border-color: var(--border-hover); }
|
|
151
|
+
.btn-default:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-default-bg-hover); border-color: var(--border-hover); }
|
|
152
|
+
.btn-ghost:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-ghost-bg-hover); border-color: transparent; color: var(--button-ghost-fg-hover); }
|
|
153
|
+
.btn-quiet:hover:not(:disabled, [aria-disabled="true"]) { background: var(--button-ghost-bg-hover); border-color: transparent; color: var(--button-ghost-fg-hover); }
|
|
154
|
+
.btn-quiet.btn-primary:hover:not(:disabled, [aria-disabled="true"]), .btn-quiet.btn-warning:hover:not(:disabled, [aria-disabled="true"]) { background: var(--yellow-component-bg-hover); border-color: transparent; color: var(--yellow-text-secondary); }
|
|
155
|
+
.btn-quiet.btn-success:hover:not(:disabled, [aria-disabled="true"]) { background: var(--green-component-bg-hover); border-color: transparent; color: var(--green-solid); }
|
|
156
|
+
.btn-quiet.btn-danger:hover:not(:disabled, [aria-disabled="true"]) { background: var(--red-component-bg-hover); border-color: transparent; color: var(--red-solid); }
|
|
157
|
+
}
|
|
148
158
|
|
|
149
159
|
/* States */
|
|
150
|
-
/* Bare disabled buttons fade
|
|
160
|
+
/* Bare disabled buttons fade. `aria-disabled="true"` counts as disabled, as
|
|
161
|
+
Base UI sets it on focusable disabled buttons; hover skips both.
|
|
151
162
|
Submit buttons are never disabled at rest: the submit validates. A filled
|
|
152
163
|
button is disabled only while its action runs (with a spinner) or once it
|
|
153
164
|
is done, so it keeps its own fill and label at full opacity: "Verifying…"
|
|
154
165
|
stays yellow, a finished 2FA "Verified" stays green with
|
|
155
166
|
data-state="success". */
|
|
156
|
-
button:disabled { opacity: 0.3; cursor: not-allowed; }
|
|
157
|
-
:is(.btn-primary, .btn-success, .btn-danger, .btn-info, .btn-invert):not(.btn-quiet):disabled { opacity: 1; }
|
|
158
|
-
.btn-default:not(.btn-quiet):disabled { opacity: 1; color: var(--solid); }
|
|
159
|
-
/* An opaque double ring: a --bg gap, then --solid-hover, ≥3:1 on page and panels.
|
|
160
|
-
|
|
167
|
+
button:is(:disabled, [aria-disabled="true"]) { opacity: 0.3; cursor: not-allowed; }
|
|
168
|
+
:is(.btn-primary, .btn-success, .btn-danger, .btn-info, .btn-invert):not(.btn-quiet):is(:disabled, [aria-disabled="true"]) { opacity: 1; }
|
|
169
|
+
.btn-default:not(.btn-quiet):is(:disabled, [aria-disabled="true"]) { opacity: 1; color: var(--solid); }
|
|
170
|
+
/* An opaque double ring: a --bg gap, then --solid-hover, ≥3:1 on page and panels.
|
|
171
|
+
Forced-colors mode drops box-shadow and paints the transparent outline. */
|
|
172
|
+
button:focus-visible { outline: 2px solid transparent; outline-offset: 2px; box-shadow: var(--shadow-focus); }
|
|
161
173
|
|
|
162
174
|
/* Loading state — hides label, paints a CSS spinner over the button */
|
|
163
|
-
button.is-loading { color: transparent !important; position: relative; pointer-events: none;
|
|
175
|
+
button.is-loading { color: transparent !important; position: relative; pointer-events: none; }
|
|
164
176
|
button.is-loading::after {
|
|
165
177
|
content: ''; position: absolute; inset: 0; margin: auto;
|
|
166
178
|
width: 14px; height: 14px; border-radius: 50%;
|
|
167
179
|
border: 1.5px solid currentColor; border-top-color: transparent;
|
|
180
|
+
color: var(--text);
|
|
168
181
|
opacity: 0.75;
|
|
169
182
|
animation: spin 0.7s linear infinite;
|
|
170
183
|
}
|
|
171
184
|
.btn-primary.is-loading::after { color: var(--primary-foreground); }
|
|
172
|
-
.btn-success.is-loading::after
|
|
173
|
-
.btn-danger.is-loading::after
|
|
174
|
-
.btn-info.is-loading::after
|
|
175
|
-
.btn-invert.is-loading::after { color: var(--
|
|
185
|
+
.btn-success.is-loading::after { color: var(--button-success-fg); }
|
|
186
|
+
.btn-danger.is-loading::after { color: var(--button-danger-fg); }
|
|
187
|
+
.btn-info.is-loading::after { color: var(--button-info-fg); }
|
|
188
|
+
.btn-invert.is-loading::after { color: var(--invert-foreground); }
|
|
176
189
|
.btn-default.is-loading::after, .btn-ghost.is-loading::after { color: var(--text); }
|
|
177
190
|
/* The spinner borrows currentColor, and .is-loading makes the label colour
|
|
178
191
|
transparent — so a quiet button needs its own spinner colour or the control
|
|
@@ -186,14 +199,14 @@ button.is-loading::after {
|
|
|
186
199
|
cards. Defined once here; do not redeclare it per card. */
|
|
187
200
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
188
201
|
|
|
189
|
-
/*
|
|
190
|
-
|
|
202
|
+
/* Typed text never takes a label's tracking or case. Tailwind v4 preflight
|
|
203
|
+
sets letter-spacing: inherit on form controls, so reset both here. */
|
|
191
204
|
input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
192
205
|
|
|
193
206
|
|
|
194
207
|
/* ===========================================================
|
|
195
208
|
Specimen header — the yellow-mono index marker that anchors
|
|
196
|
-
each preview card.
|
|
209
|
+
each preview card.
|
|
197
210
|
=========================================================== */
|
|
198
211
|
.head {
|
|
199
212
|
display: flex; align-items: baseline; gap: 16px;
|
|
@@ -221,13 +234,11 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
221
234
|
pages take neither: see .auth-page.
|
|
222
235
|
|
|
223
236
|
.panel = quiet inline reference container. Sits ON --bg,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
panels, callouts.
|
|
237
|
+
reads --panel-* so consumers can retheme it apart
|
|
238
|
+
from cards; today the values match. Code blocks,
|
|
239
|
+
info asides, sidebar panels, callouts.
|
|
227
240
|
|
|
228
|
-
|
|
229
|
-
ONLY thing that differs is the surface fill. Pick the role,
|
|
230
|
-
not the chrome.
|
|
241
|
+
Pick the role, not the chrome.
|
|
231
242
|
=========================================================== */
|
|
232
243
|
.card {
|
|
233
244
|
background: var(--component-bg);
|
|
@@ -252,10 +263,8 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
252
263
|
/* ===========================================================
|
|
253
264
|
Field primitives — production parity with app.put.io forms.
|
|
254
265
|
|
|
255
|
-
ONE source of truth for input chrome.
|
|
256
|
-
|
|
257
|
-
now they all inherit from here. To override per-screen, change
|
|
258
|
-
geometry (height/padding) — not the surface tokens.
|
|
266
|
+
ONE source of truth for input chrome. To override per-screen,
|
|
267
|
+
change geometry (height/padding) — not the surface tokens.
|
|
259
268
|
|
|
260
269
|
Surface contract (filled, so a field reads on any surface):
|
|
261
270
|
bg → --field-bg (= --bg-secondary)
|
|
@@ -290,10 +299,12 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
290
299
|
background var(--dur-fast) var(--ease-out);
|
|
291
300
|
}
|
|
292
301
|
.field::placeholder { color: var(--field-placeholder); }
|
|
293
|
-
|
|
302
|
+
@media (hover: hover) and (pointer: fine) {
|
|
303
|
+
.field:hover:where(:not(:disabled, [aria-disabled="true"])) { border-color: var(--field-border-hover); }
|
|
304
|
+
}
|
|
294
305
|
.field:focus,
|
|
295
306
|
.field:focus-visible {
|
|
296
|
-
outline:
|
|
307
|
+
outline: 2px solid transparent;
|
|
297
308
|
border-color: var(--field-border-focus);
|
|
298
309
|
box-shadow: var(--field-ring);
|
|
299
310
|
}
|
|
@@ -312,8 +323,8 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
312
323
|
border-color: var(--red-border);
|
|
313
324
|
transition-duration: var(--dur-reveal);
|
|
314
325
|
}
|
|
315
|
-
|
|
316
|
-
border-color: var(--red-border-hover);
|
|
326
|
+
@media (hover: hover) and (pointer: fine) {
|
|
327
|
+
.field[aria-invalid="true"]:hover { border-color: var(--red-border-hover); }
|
|
317
328
|
}
|
|
318
329
|
.field[aria-invalid="true"]:focus,
|
|
319
330
|
.field[aria-invalid="true"]:focus-visible {
|
|
@@ -381,8 +392,10 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
381
392
|
text-decoration: none;
|
|
382
393
|
white-space: nowrap;
|
|
383
394
|
}
|
|
384
|
-
|
|
385
|
-
.form-group .label-
|
|
395
|
+
@media (hover: hover) and (pointer: fine) {
|
|
396
|
+
.form-group > .label-action:hover,
|
|
397
|
+
.form-group .label-row a:hover { color: var(--text); }
|
|
398
|
+
}
|
|
386
399
|
|
|
387
400
|
/* ===========================================================
|
|
388
401
|
Form feedback — one public recipe for block and inline status.
|
|
@@ -406,6 +419,7 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
406
419
|
color: var(--alert-info-body);
|
|
407
420
|
font-size: var(--fs-ui);
|
|
408
421
|
line-height: var(--lh-normal);
|
|
422
|
+
overflow-wrap: anywhere;
|
|
409
423
|
}
|
|
410
424
|
.form-callout[hidden] { display: none; }
|
|
411
425
|
.form-callout > :is(i, svg) {
|
|
@@ -484,6 +498,7 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
484
498
|
gap: clamp(4px, 2vw, 8px);
|
|
485
499
|
width: 100%;
|
|
486
500
|
min-width: 0;
|
|
501
|
+
isolation: isolate;
|
|
487
502
|
}
|
|
488
503
|
.otp-input {
|
|
489
504
|
position: absolute;
|
|
@@ -495,6 +510,7 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
495
510
|
margin: 0;
|
|
496
511
|
padding: 0;
|
|
497
512
|
border: 0;
|
|
513
|
+
font-size: var(--fs-field);
|
|
498
514
|
opacity: 0;
|
|
499
515
|
cursor: text;
|
|
500
516
|
}
|
|
@@ -531,6 +547,9 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
531
547
|
border-color: var(--field-border-focus);
|
|
532
548
|
box-shadow: var(--field-ring);
|
|
533
549
|
}
|
|
550
|
+
@media (forced-colors: active) {
|
|
551
|
+
.otp:focus-within .otp-slot[data-state="active"] { outline: 2px solid Highlight; }
|
|
552
|
+
}
|
|
534
553
|
.otp-separator {
|
|
535
554
|
flex: 0 0 auto;
|
|
536
555
|
padding: 0;
|
|
@@ -617,7 +636,7 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
617
636
|
.auth-title {
|
|
618
637
|
margin: 0 0 28px;
|
|
619
638
|
font-size: var(--fs-title);
|
|
620
|
-
line-height:
|
|
639
|
+
line-height: calc(30 / 22);
|
|
621
640
|
font-weight: var(--fw-medium);
|
|
622
641
|
letter-spacing: var(--tracking-snug);
|
|
623
642
|
color: var(--text);
|
|
@@ -667,9 +686,11 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
667
686
|
text-decoration: none;
|
|
668
687
|
text-underline-offset: 3px;
|
|
669
688
|
}
|
|
670
|
-
.auth-links a:hover { text-decoration: underline; }
|
|
671
689
|
.auth-links > a { color: var(--text-secondary); font-weight: inherit; }
|
|
672
|
-
|
|
690
|
+
@media (hover: hover) and (pointer: fine) {
|
|
691
|
+
.auth-links a:hover { text-decoration: underline; }
|
|
692
|
+
.auth-links > a:hover { color: var(--text); text-decoration: none; }
|
|
693
|
+
}
|
|
673
694
|
/* Two items split to the edges; a lone link or prompt centres. */
|
|
674
695
|
.auth-links > :only-child { margin-inline: auto; text-align: center; }
|
|
675
696
|
/* A back link leads with Phosphor's arrow-left, an inline SVG (or the icon font). */
|
|
@@ -685,7 +706,17 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
685
706
|
font-size: var(--fs-caption);
|
|
686
707
|
}
|
|
687
708
|
.auth-footer a { color: var(--text-secondary); text-decoration: none; }
|
|
688
|
-
|
|
709
|
+
@media (hover: hover) and (pointer: fine) {
|
|
710
|
+
.auth-footer a:hover { color: var(--text); }
|
|
711
|
+
}
|
|
712
|
+
.form-group > .label-action:focus-visible,
|
|
713
|
+
.form-group .label-row a:focus-visible,
|
|
714
|
+
.auth-links a:focus-visible,
|
|
715
|
+
.auth-footer a:focus-visible {
|
|
716
|
+
outline: 2px solid var(--solid-hover);
|
|
717
|
+
outline-offset: 2px;
|
|
718
|
+
border-radius: var(--radius-sm);
|
|
719
|
+
}
|
|
689
720
|
/* A middot centres in the gap before each link after the first. The empty
|
|
690
721
|
alt keeps it out of the link's accessible name. */
|
|
691
722
|
.auth-footer > * + * { position: relative; }
|
|
@@ -742,25 +773,35 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
742
773
|
`.on` is exactly the invented flag the state contract tells
|
|
743
774
|
you not to add — prefer the attribute in new markup.
|
|
744
775
|
|
|
745
|
-
.checkbox —
|
|
776
|
+
.checkbox — <button role="checkbox">, aria-checked or data-state="checked"
|
|
746
777
|
input[type="checkbox"] — native inputs get the brand accent
|
|
747
|
-
.switch —
|
|
778
|
+
.switch — <button role="switch">, aria-checked or data-state="checked".
|
|
779
|
+
Resize it with --switch-width and --switch-height; the thumb
|
|
780
|
+
and its travel follow.
|
|
748
781
|
=========================================================== */
|
|
749
782
|
.checkbox {
|
|
783
|
+
position: relative;
|
|
784
|
+
/* Buttons default to border-box; a span and a button draw the same box. */
|
|
785
|
+
box-sizing: content-box;
|
|
750
786
|
width: 14px; height: 14px; flex-shrink: 0;
|
|
751
|
-
|
|
787
|
+
padding: 0;
|
|
788
|
+
border: 1.5px solid var(--solid);
|
|
752
789
|
border-radius: var(--radius-sm);
|
|
753
790
|
background: transparent;
|
|
791
|
+
cursor: pointer;
|
|
754
792
|
transition: background var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out);
|
|
755
793
|
}
|
|
794
|
+
/* Pads the 14px box out to the 24px minimum target (WCAG 2.5.8). */
|
|
795
|
+
.checkbox::before { content: ''; position: absolute; inset: -5px; }
|
|
756
796
|
.checkbox.checked,
|
|
757
|
-
.checkbox[data-state="checked"]
|
|
797
|
+
.checkbox[data-state="checked"],
|
|
798
|
+
.checkbox[aria-checked="true"] {
|
|
758
799
|
background: var(--yellow-solid);
|
|
759
800
|
border-color: var(--yellow-solid);
|
|
760
|
-
position: relative;
|
|
761
801
|
}
|
|
762
802
|
.checkbox.checked::after,
|
|
763
|
-
.checkbox[data-state="checked"]::after
|
|
803
|
+
.checkbox[data-state="checked"]::after,
|
|
804
|
+
.checkbox[aria-checked="true"]::after {
|
|
764
805
|
content: ''; position: absolute; left: 3px; top: 0;
|
|
765
806
|
width: 5px; height: 9px;
|
|
766
807
|
border: solid var(--primary-foreground); border-width: 0 2px 2px 0;
|
|
@@ -769,7 +810,9 @@ input, textarea, select { text-transform: none; letter-spacing: normal; }
|
|
|
769
810
|
input[type="checkbox"], input[type="radio"] { accent-color: var(--yellow-solid); cursor: pointer; }
|
|
770
811
|
|
|
771
812
|
.switch {
|
|
772
|
-
width: 44px; height: 24px; flex-shrink: 0;
|
|
813
|
+
width: var(--switch-width, 44px); height: var(--switch-height, 24px); flex-shrink: 0;
|
|
814
|
+
padding: 0;
|
|
815
|
+
border: 0;
|
|
773
816
|
border-radius: var(--radius-pill);
|
|
774
817
|
background: var(--component-bg-active);
|
|
775
818
|
position: relative; cursor: pointer;
|
|
@@ -777,13 +820,17 @@ input[type="checkbox"], input[type="radio"] { accent-color: var(--yellow-solid);
|
|
|
777
820
|
}
|
|
778
821
|
.switch::after {
|
|
779
822
|
content: ''; position: absolute; top: 2px; left: 2px;
|
|
780
|
-
width:
|
|
823
|
+
width: calc(var(--switch-height, 24px) - 4px); height: calc(var(--switch-height, 24px) - 4px); border-radius: 50%;
|
|
781
824
|
background: var(--text);
|
|
782
|
-
transition:
|
|
825
|
+
transition: transform var(--dur-fast) var(--ease-out);
|
|
783
826
|
}
|
|
784
827
|
.switch.on,
|
|
785
|
-
.switch[data-state="checked"]
|
|
828
|
+
.switch[data-state="checked"],
|
|
829
|
+
.switch[aria-checked="true"] { background: var(--green-solid); }
|
|
786
830
|
/* The thumb rides the green selected track, so it takes the token for "what
|
|
787
831
|
colour sits on green" rather than a raw white that no theme can reach. */
|
|
788
832
|
.switch.on::after,
|
|
789
|
-
.switch[data-state="checked"]::after
|
|
833
|
+
.switch[data-state="checked"]::after,
|
|
834
|
+
.switch[aria-checked="true"]::after { transform: translateX(calc(var(--switch-width, 44px) - var(--switch-height, 24px))); background: var(--button-success-fg); }
|
|
835
|
+
.checkbox:focus-visible,
|
|
836
|
+
.switch:focus-visible { outline: 2px solid transparent; outline-offset: 2px; box-shadow: var(--shadow-focus); }
|
package/tokens/foundation.css
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
/* -- Document base ------------------------------------------------- */
|
|
2
|
+
/* Native scrollbars, controls and autofill follow the mode. */
|
|
3
|
+
:root { color-scheme: light; }
|
|
4
|
+
.dark { color-scheme: dark; }
|
|
2
5
|
html {
|
|
3
6
|
background: var(--html-bg);
|
|
4
7
|
color: var(--text);
|
|
@@ -33,7 +36,6 @@ body {
|
|
|
33
36
|
.num, .tabular {
|
|
34
37
|
font-family: var(--font-mono);
|
|
35
38
|
font-variant-numeric: tabular-nums;
|
|
36
|
-
font-feature-settings: "tnum";
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
h1, .h1 {
|