mirror of
https://github.com/supabase/supabase.git
synced 2026-09-06 01:49:12 +08:00
## What kind of change does this PR introduce? Bug fix / design-system token hygiene for form and selector chrome. ## What is the current behavior? After opaque default-button fills, text fields, selects, and selector tiles drifted apart: inputs and selects mixed ad-hoc washes, hover borders bounced between `border-stronger` / `border-foreground-muted`, invalid fields had no hover step, and composites like InputGroup leaked inner hover borders. Follow-up to #48837 (opaque button fills) where Select rest still felt darker than Input on forms such as scoped access tokens. ## What is the new behavior? Named control roles and one interactive border: | Role | Fill | Rest border | Hover / focus / open | | --- | --- | --- | --- | | Field (sunk) | `bg-field` | `border-control` | `border-control-hover` | | Raised control | `bg-control-raised` | `border-strong` | `border-control-hover` | | Overlaying action | card → popover | `border-strong` | `border-control-hover` | | Invalid field | `bg-destructive-200` | `border-destructive-400` | `border-destructive` | - `--field` / `--control-raised` / `--border-control-hover` live in `semantic.css` (source of truth for roles; README points there) - Input / Textarea / InputGroup / legacy TextArea use the field ladder (incl. invalid hover) - Select and empty MultiSelect use raised; filled MultiSelect sinks to field - Default + dashed Button, CommandMenu trigger, and radio card/stacked/large use `border-control-hover` - Studio selector tiles aligned: Connect mode, role impersonation, DuckLake modes, compute “Contact us” | Before and After | | --- | | <img width="1576" height="759" alt="Access Tokens Account Supabase" src="https://github.com/user-attachments/assets/4bbe8b2b-a31a-4d63-80ba-04a1a8a5609d" /> | | <img width="1576" height="759" alt="Access Tokens Account Supabase" src="https://github.com/user-attachments/assets/92271223-4103-4cc6-a7c0-9e4ff71cce30" /> | ## Additional context `--control-raised` aliases `--card` today (role name so fill can diverge later). Rest `border-control` / `border-strong` both still map to `--input` via compat; the shared interactive step is `--border-control-hover`. ## To test 1. **[Account → Access Tokens](https://studio-staging-git-dnywh-fixcontrol-surface-tokens-supabase.vercel.app/dashboard/account/tokens)** Open Generate / New scoped token. Side-by-side Input, Select, RadioGroupStacked, MultiSelect. Confirm sunk vs raised fills, shared hover border, MultiSelect flips to sunk once a value is selected. Leave a required field empty to check invalid rest → hover → focus. 2. **[Org → Projects](https://studio-staging-git-dnywh-fixcontrol-surface-tokens-supabase.vercel.app/dashboard/org/_)** Hover the dashed Status filter. Hover default / filled filter buttons when active. Confirm hover/open borders match. 3. **[Project → Connect](https://studio-staging-git-dnywh-fixcontrol-surface-tokens-supabase.vercel.app/dashboard/project/_)** Open Connect from the header. Mode grid tiles: hover + selected borders match radio cards (no old muted-foreground ring). 4. **[Project → Compute](https://studio-staging-git-dnywh-fixcontrol-surface-tokens-supabase.vercel.app/dashboard/project/_/settings/infrastructure)** (optional) Compute size radios + “Contact us” tile hover.
268 lines
13 KiB
CSS
268 lines
13 KiB
CSS
/*
|
||
* Core semantic color system.
|
||
*
|
||
* Themes override the small set of inputs below; every semantic color is then
|
||
* derived from them in OKLCH. The stepped numeric scales (brand / warning /
|
||
* destructive / secondary) are NOT derived here — they live as per-theme values
|
||
* in the theme files and are mapped to utilities in packages/config/css/theme.css.
|
||
*/
|
||
|
||
:root {
|
||
/*
|
||
* Hue inputs. A single --hue used to drive both the neutral surface/text ramp
|
||
* and the brand, so the two could never drift apart. They are now split so a
|
||
* theme can run, say, a cool-gray surface against the green brand:
|
||
* --surface-hue tints backgrounds, text and borders (the neutral ramp)
|
||
* --primary-hue is the brand hue that --primary is built from
|
||
* Both default to --hue (Supabase green ≈159° in OKLCH), so a theme that only
|
||
* sets --hue keeps surfaces and brand locked together as before; override
|
||
* either one to let them diverge. */
|
||
--hue: 159;
|
||
--surface-hue: var(--hue);
|
||
--primary-hue: var(--hue);
|
||
/* Root defaults mirror the dark theme (the app's default appearance). */
|
||
--chroma: 0.016;
|
||
--surface: 0.17;
|
||
|
||
--foreground-lightness: 0.95;
|
||
/* How far muted/tertiary text sits from --surface toward --foreground-lightness
|
||
(0 = surface, 1 = full foreground) at --contrast 0.5; themes override. */
|
||
--muted-foreground-level: 0.8;
|
||
--tertiary-foreground-level: 0.65;
|
||
|
||
/*
|
||
* Global contrast knob, 0 to 1. Default 0.5 is the comfortable baseline; 0 keeps
|
||
* borders and accent barely visible over muted surfaces; 1 pushes them to full
|
||
* strength. Foreground hits --foreground-lightness at 0.5; below that it eases
|
||
* toward the surface down to --contrast-text-floor at 0. Muted and tertiary
|
||
* foreground lightness sits at the theme base level (a surface→foreground mix) at
|
||
* 0.5 and eases up toward foreground or down toward half that base as contrast moves.
|
||
*
|
||
* --contrast-delta is -1 at 0, 0 at 0.5, +1 at 1 — shared offset from baseline;
|
||
* --contrast-up / --contrast-down are its positive / negative halves.
|
||
* --contrast-border ramps [floor, 1] over --contrast and squares it so most of
|
||
* the range lives toward full strength. ((delta + 1) / 2 simplifies to contrast.)
|
||
*/
|
||
--contrast: 0.5;
|
||
--contrast-delta: calc((var(--contrast) - 0.5) / 0.5);
|
||
/* positive and negative halves of the delta, reused below */
|
||
--contrast-up: max(0, var(--contrast-delta));
|
||
--contrast-down: max(0, calc(var(--contrast-delta) * -1));
|
||
--contrast-text-floor: 0.9;
|
||
--contrast-border-floor: 0.05;
|
||
--contrast-text: calc(1 - (1 - var(--contrast-text-floor)) * var(--contrast-down));
|
||
--contrast-border-linear: calc(
|
||
var(--contrast-border-floor) + (1 - var(--contrast-border-floor)) * var(--contrast)
|
||
);
|
||
--contrast-border: calc(var(--contrast-border-linear) * var(--contrast-border-linear));
|
||
|
||
/*
|
||
* Surface elevation. Each surface steps its lightness away from --background
|
||
* by `--elevation-step * ratio`, where the ratios are shared across themes and
|
||
* the step is signed: positive brightens as surfaces rise, negative darkens.
|
||
*
|
||
* The default step reproduces the historical behavior (≈3% per level mixed
|
||
* toward the foreground): dark themes brighten and the light baseline darkens.
|
||
* The light theme overrides --elevation-step (and lowers --surface for
|
||
* headroom) so its higher surfaces brighten toward white instead — giving a
|
||
* single "higher = lighter" model in both modes. 0 = flat (no elevation).
|
||
*/
|
||
--elevation-1: 1;
|
||
--elevation-2: 1.5;
|
||
--elevation-3: 2;
|
||
--elevation-4: 3;
|
||
/* Signed span surface→foreground (+ in dark, − in light). */
|
||
--tone-span: calc(var(--foreground-lightness) - var(--surface));
|
||
--elevation-step: calc(var(--tone-span) * 0.04);
|
||
/*
|
||
* Foreground overlays for muted / accent / tertiary. Alpha is tuned so each level
|
||
* matches the lightness delta of the same elevation ratio on --background; a
|
||
* translucent --foreground layer then composites with whatever surface sits
|
||
* beneath (lightens in dark, darkens in light). Steps: muted (e1) → accent
|
||
* (e2) → tertiary (e3).
|
||
*/
|
||
/* max(span, -span) is equivalent to abs(span), but works across the browser
|
||
versions supported by Tailwind v4. */
|
||
--surface-overlay-unit: calc(
|
||
var(--elevation-step) / max(var(--tone-span), calc(var(--tone-span) * -1))
|
||
);
|
||
--muted-alpha: calc(var(--surface-overlay-unit) * var(--elevation-1));
|
||
--accent-alpha: calc(var(--surface-overlay-unit) * var(--elevation-2));
|
||
--tertiary-alpha: calc(var(--surface-overlay-unit) * var(--elevation-3));
|
||
|
||
/*
|
||
* Status hues stay anchored to amber / red but pull a fraction of the way
|
||
* toward the brand --primary-hue for harmony, then clamp to their perceptual
|
||
* category so "warning" always reads amber and "destructive" always reads red
|
||
* no matter what --primary-hue is. --status-hue-pull: 0 = fixed amber/red,
|
||
* 1 = fully brand-relative. At the default --primary-hue the pull term is 0, so
|
||
* these resolve to their bases (warning 75 / destructive 25 / info 288).
|
||
* --brand-hue-reference is the --primary-hue at which pull is zero; keep it
|
||
* equal to the default brand hue.
|
||
* Warning sits at 75 (amber), not higher toward yellow — yellow can't darken
|
||
* without going olive, so it loses contrast on light surfaces. Info is a violet
|
||
* (matching the "no additional charge" badge); like the others it pulls a touch
|
||
* with the brand --primary-hue but clamps so it always reads violet.
|
||
*/
|
||
--brand-hue-reference: 159;
|
||
--status-hue-pull: 0.15;
|
||
--warning-hue: clamp(
|
||
65,
|
||
calc(75 + (var(--primary-hue) - var(--brand-hue-reference)) * var(--status-hue-pull)),
|
||
95
|
||
);
|
||
--destructive-hue: clamp(
|
||
12,
|
||
calc(25 + (var(--primary-hue) - var(--brand-hue-reference)) * var(--status-hue-pull)),
|
||
42
|
||
);
|
||
--info-hue: clamp(
|
||
272,
|
||
calc(288 + (var(--primary-hue) - var(--brand-hue-reference)) * var(--status-hue-pull)),
|
||
302
|
||
);
|
||
/*
|
||
* Expressive (status) colors are deliberately NOT derived from the neutral
|
||
* surface→foreground span the way text and surfaces are. An expressive color
|
||
* has its own identity; tying its lightness to the theme's text ramp is what
|
||
* left warning under-saturated in light mode and too dark in dark mode.
|
||
*
|
||
* Instead each is anchored to a fixed target lightness near its hue's chroma
|
||
* peak and a single shared --expressive-chroma. That chroma is a flat constant,
|
||
* deliberately NOT derived from the neutral --chroma knob: status colors must
|
||
* stay legible even when the brand is dialed toward grayscale. It sits inside
|
||
* sRGB at these lightnesses, so no gamut mapping is required.
|
||
*
|
||
* The dark-mode lightness anchors live here; light mode runs darker (set in
|
||
* light.css) so the fills hold contrast against the near-white surface.
|
||
*/
|
||
--expressive-chroma: 0.14;
|
||
--warning-lightness: 0.8;
|
||
--destructive-lightness: 0.75;
|
||
--info-lightness: 0.7;
|
||
|
||
/* shadcn semantic pairs */
|
||
--background: oklch(var(--surface) calc(var(--chroma) * 0.5) var(--surface-hue));
|
||
--foreground-chroma: calc(var(--chroma) * 0.55);
|
||
--foreground: oklch(
|
||
calc(var(--surface) + var(--tone-span) * var(--contrast-text)) var(--foreground-chroma)
|
||
var(--surface-hue)
|
||
);
|
||
/* Inverse of --foreground: canvas-coloured text for use on inverted fills
|
||
(bg-foreground, bg-black / dark:bg-white, etc). Not the same as
|
||
--primary-foreground, which stays dark for the vivid primary fill. */
|
||
--foreground-contrast: oklch(var(--surface) calc(var(--chroma) * 0.55) var(--surface-hue));
|
||
|
||
--card: oklch(from var(--background) calc(l + var(--elevation-step) * var(--elevation-1)) c h);
|
||
--card-foreground: var(--foreground);
|
||
--popover: oklch(from var(--background) calc(l + var(--elevation-step) * var(--elevation-2)) c h);
|
||
--popover-foreground: var(--foreground);
|
||
|
||
/* Primary is the brand-derived accent: a fixed, vivid Supabase green on
|
||
--primary-hue, mode-invariant and decoupled from the --chroma knob so it
|
||
holds its identity even as neutrals desaturate. NOTE: the `brand` utility
|
||
(--color-brand in theme.css) is deliberately NOT this — it maps to the
|
||
per-theme --brand-default scale (master behavior) while we resolve how
|
||
`brand` is used across the interface. */
|
||
--primary: oklch(0.76 0.15 var(--primary-hue));
|
||
--primary-foreground: oklch(
|
||
min(var(--surface), var(--foreground-lightness)) calc(var(--chroma) * 0.45) var(--primary-hue)
|
||
);
|
||
|
||
--secondary: oklch(
|
||
from var(--background) calc(l + var(--elevation-step) * var(--elevation-3)) c h
|
||
);
|
||
--secondary-foreground: var(--foreground);
|
||
--tertiary: oklch(from var(--foreground) l c h / var(--tertiary-alpha));
|
||
--tertiary-foreground-mix: calc(
|
||
var(--tertiary-foreground-level) + (1 - var(--tertiary-foreground-level)) * var(--contrast-up) -
|
||
var(--tertiary-foreground-level) * 0.5 * var(--contrast-down)
|
||
);
|
||
--tertiary-foreground: oklch(
|
||
calc(var(--surface) + var(--tone-span) * var(--tertiary-foreground-mix))
|
||
var(--foreground-chroma) var(--surface-hue)
|
||
);
|
||
--muted: oklch(from var(--foreground) l c h / var(--muted-alpha));
|
||
--muted-foreground-mix: calc(
|
||
var(--muted-foreground-level) + (1 - var(--muted-foreground-level)) * var(--contrast-up) -
|
||
var(--muted-foreground-level) * 0.5 * var(--contrast-down)
|
||
);
|
||
--muted-foreground: oklch(
|
||
calc(var(--surface) + var(--tone-span) * var(--muted-foreground-mix)) var(--foreground-chroma)
|
||
var(--surface-hue)
|
||
);
|
||
--accent: oklch(from var(--foreground) l c h / var(--accent-alpha));
|
||
--accent-foreground: var(--foreground);
|
||
|
||
/*
|
||
* Control surface roles (design sync: sunk fields + raised choosers):
|
||
* --field sunk text entry (Input, Textarea, filled MultiSelect)
|
||
* --control-raised raised chooser plate (Select, empty MultiSelect)
|
||
* --control alias of --control-raised
|
||
*
|
||
* Alpha overlays relative to the local plate (page, sheet, card) — not solids
|
||
* from --background. Page-relative solids sit between canvas (e0) and Sheet
|
||
* (popover e2) and vanish on light near the white ceiling.
|
||
*
|
||
* Field soft always-sink via black. --field-alpha is a theme input (stronger in
|
||
* dark.css) because the same absolute opacity is obvious on light plates and
|
||
* near-invisible on dark ones. Default is softer than the old foreground/[.026]
|
||
* wash. Raised is a soft white lift so selects read button-like without stark
|
||
* wells. Borders carry most hover chrome (--border-control-hover). Light raises
|
||
* still have little headroom on sheets; sheets matching app background would
|
||
* widen that later.
|
||
*
|
||
* Default Button stays opaque card/popover for occluding chrome (DEPR-636).
|
||
* Floating clusters can wrap in a solid Popover as a follow-up.
|
||
*/
|
||
--field-alpha: 0.015;
|
||
--field: oklch(0 0 0 / var(--field-alpha));
|
||
--control-raised: oklch(1 0 0 / calc(var(--muted-alpha) * 0.65));
|
||
--control: var(--control-raised);
|
||
|
||
--warning: oklch(var(--warning-lightness) var(--expressive-chroma) var(--warning-hue));
|
||
--destructive: oklch(
|
||
var(--destructive-lightness) var(--expressive-chroma) var(--destructive-hue)
|
||
);
|
||
/*
|
||
* Text that sits on the solid fill: dark ink on light fills (amber), white on
|
||
* dark fills (red). The clamp turns the fill's own lightness into a hard flip
|
||
* around L≈0.62 — (0.62 − l) is negative for light fills (→ floor 0.12) and
|
||
* positive for dark fills (→ ceil 0.99) — so on-colors stay legible and keep
|
||
* flipping correctly if the anchors are retuned. A trace of hue chroma keeps
|
||
* the ink from reading as flat gray.
|
||
*/
|
||
--warning-foreground: oklch(
|
||
from var(--warning) clamp(0.12, calc((0.62 - l) * 100), 0.99) calc(c * 0.08) h
|
||
);
|
||
--destructive-foreground: oklch(
|
||
from var(--destructive) clamp(0.12, calc((0.62 - l) * 100), 0.99) calc(c * 0.08) h
|
||
);
|
||
--info: oklch(var(--info-lightness) var(--expressive-chroma) var(--info-hue));
|
||
--info-foreground: oklch(
|
||
from var(--info) clamp(0.12, calc((0.62 - l) * 100), 0.99) calc(c * 0.08) h
|
||
);
|
||
|
||
/* Status border tokens for subtle / tinted surfaces */
|
||
--border-destructive: oklch(from var(--destructive) l c h / 30%);
|
||
--border-warning: oklch(from var(--warning) l c h / 30%);
|
||
--border-info: oklch(from var(--info) l c h / 30%);
|
||
--border-brand: oklch(from var(--primary) l c h / 30%);
|
||
|
||
--border: oklch(
|
||
from var(--foreground) l calc(c * 0.54) h / calc(2% + 20% * var(--contrast-border))
|
||
);
|
||
--input: oklch(
|
||
from var(--foreground) l calc(c * 0.5) h / calc(3% + 38% * var(--contrast-border))
|
||
);
|
||
/*
|
||
* Shared interactive border for control chrome (inputs, selects, default /
|
||
* dashed Button, radio cards). Pairs with rest --border-control / --border-strong.
|
||
* Stronger than --border-stronger; softer than a full muted-foreground stroke.
|
||
*/
|
||
--border-control-hover: oklch(
|
||
from var(--foreground) l calc(c * 0.5) h / calc(12% + 58% * var(--contrast-border))
|
||
);
|
||
--ring: oklch(from var(--primary) l c h / 55%);
|
||
}
|