Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2146d85
Agentic UI: Add spacing, line-height, type, radius, elevation and lay…
youknowriad Oct 25, 2026
57bdf96
Merge branch 'trunk' of https://github.com/Automattic/studio into add…
youknowriad Oct 25, 2026
002f951
Agentic UI: Share CSS recipes and keyframes, and size icons without a…
youknowriad Oct 25, 2026
ec53659
Agentic UI: Step nested surfaces automatically and scope dark-mode co…
youknowriad Oct 25, 2026
842146c
Merge branch 'trunk' of https://github.com/Automattic/studio into sco…
youknowriad Oct 25, 2026
76eb665
Merge branch 'trunk' of https://github.com/Automattic/studio into sha…
youknowriad Oct 25, 2026
52e4499
Document the new tokens
youknowriad Oct 25, 2026
998a3c8
Document scheme scoping and surface stepping
youknowriad Oct 25, 2026
643018b
Layer the recipes with the primitives so component rules override them
youknowriad Oct 25, 2026
86b700a
Merge branch 'trunk' of https://github.com/Automattic/studio into add…
youknowriad Oct 25, 2026
1192279
Merge branch 'trunk' of https://github.com/Automattic/studio into sha…
youknowriad Oct 25, 2026
8c64de7
Merge branch 'trunk' of https://github.com/Automattic/studio into sco…
youknowriad Oct 25, 2026
cc55bb4
Agentic UI: Lint and format CSS with ESLint, and require theme tokens…
youknowriad Oct 25, 2026
c8b8bd0
Merge branch 'add-css-design-tokens' into lint-css-new
youknowriad Oct 25, 2026
b916afe
Merge branch 'share-css-recipes' into lint-css-new
youknowriad Oct 25, 2026
b247537
Merge branch 'scope-css-surfaces-and-schemes' into lint-css-new
youknowriad Oct 25, 2026
6de2081
Format CSS with Prettier through ESLint and record the guard's except…
youknowriad Oct 25, 2026
f2dd1d0
Merge branch 'trunk' of https://github.com/Automattic/studio into css…
youknowriad Oct 25, 2026
8119f42
Format the token rule
youknowriad Oct 25, 2026
6026f78
Agentic UI: Lint feature code that reaches into the UI primitives
youknowriad Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions apps/ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,24 @@ UI is built on Base UI with its own primitives and theme. Icons come from `lucid
The theme lives in `src/lib/theme/`:

- `tokens.css`: every design token, below.
- `base.css`: the page and element defaults (fonts, body, headings, icon size, reduced transparency).
- `base.css`: the page and element defaults (fonts, body, headings, shared keyframes, reduced transparency).
- `recipes.module.css`: declaration groups that components pull in with `composes` (truncation, the band's hairline, the image outline, the dot on an icon).
- `index.ts`: applies the color scheme and the Appearance settings to the root; `accent-contrast.ts` and `color.ts` keep the accent readable; `motion.ts` reads the motion tokens from script.

`tokens.css` defines every token the UI uses:

- **Surfaces**, from lightest to darkest in light mode: `--surface-0`…`--surface-4`, named by role as `--background`, `--chrome` (sidebar, header band), `--chat`, `--composer`, `--popover` and `--muted` (cards, code, chips). `--hover` is the hover fill of the surface it sits on.
- **Text and lines**: `--foreground`, `--muted-foreground`, `--border`, `--input`, `--ring`.
- **Actions and status**: `--primary`, `--secondary`, `--accent` (and `--accent-text`, `--accent-foreground`), `--destructive`, `--success`, `--warning`, plus `--status-*` for the vivid status dots.
- **Type, shape and motion**: `--font-sans`, `--font-mono`, `--text-xxs`…`--text-xl`, `--font-weight-*`, `--radius-*`, `--shadow-*`, `--z-*` layers, `--focus-width`, `--band-height`, `--control-size`, `--motion-duration-*` and `--motion-ease-*`.
- **Type**: `--font-sans`, `--font-mono`, `--text-2xs`…`--text-2xl` (with `--text-*--line-height` where a size has its own line height), `--leading-*` and `--font-weight-*`.
- **Space and shape**: `--space-*` on a 4px grid, numbered like Tailwind's spacing (`--space-1_5` is 6px), `--radius-*` up to `--radius-3xl` and `--radius-full`, `--shadow-*`, `--focus-width`, `--band-height` and `--control-size`.
- **Layers and motion**: `--z-*` layers inside the workspace and above it, `--motion-duration-*` and `--motion-ease-*`. Media queries can't read tokens, so the one breakpoint, `( width < 640px )`, is written where it's used.

Primitives live in `src/lib/ui/` (Button, IconButton, Dialog, Menu, Popover, Select, Field, Input, InputGroup, Textarea, Switch, Checkbox, Tooltip, Alert, Badge, Progress, Empty, Kbd, VisuallyHidden). They follow the rebuild's kit, sit in the `ui` cascade layer so feature CSS overrides them without specificity tricks, and import nothing from app code.

Colors derive in CSS from a few inputs on the root element (`--tint-hue`, `--tint-intensity`, `--tint-saturation`, `--contrast`, `--accent-fill`), so light, dark and the Appearance settings change them without any palette code. Dark values apply under `[data-color-scheme='dark']`, on the root or on any element that should render in the other scheme.
Outside `src/lib/theme/`, ESLint (`studio/use-theme-tokens`) flags literal colors, font sizes, radii, elevation shadows and stacking layers, and formats CSS with Prettier, so `npx eslint --fix` covers stylesheets too. A literal that has to stay carries an `eslint-disable-next-line` comment saying why.

Colors derive in CSS from a few inputs on the root element (`--tint-hue`, `--tint-intensity`, `--tint-saturation`, `--contrast`, `--accent-fill`), so light, dark and the Appearance settings change them without any palette code. Dark values apply under `[data-color-scheme='dark']`, on the root or on any element that should render in the other scheme. Components follow the nearest scheme with `light-dark()` for a color, or a `[data-color-scheme='dark'] .x` selector for anything else; never `:root[...]` or `html[...]`, which ignore a scene's own scheme. A surface sets `--surface` to its fill, and `--muted` inside it steps up a shade on its own.

## Development

Expand Down
9 changes: 6 additions & 3 deletions apps/ui/src/app/app-providers.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { QueryClientProvider } from '@tanstack/react-query';
import { defaultI18n } from '@wordpress/i18n';
import { I18nProvider } from '@wordpress/react-i18n';
import { LucideProvider } from 'lucide-react';
import { useLayoutEffect } from 'react';
import { AgentRunProvider } from '@/data/agent-runs/provider';
import { ConnectorProvider, queryClient } from '@/data/core';
Expand Down Expand Up @@ -48,9 +49,11 @@ function ThemedApp( { children }: PropsWithChildren ) {
[ colorScheme, accentColor, tintIntensity, contrast ]
);
return (
<TooltipProvider>
<OnboardingGuideProvider>{ children }</OnboardingGuideProvider>
</TooltipProvider>
<LucideProvider size={ 16 }>
<TooltipProvider>
<OnboardingGuideProvider>{ children }</OnboardingGuideProvider>
</TooltipProvider>
</LucideProvider>
);
}

Expand Down
54 changes: 28 additions & 26 deletions apps/ui/src/features/account/account-prompt/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
flex-direction: column;
align-items: center;
width: 100%;
margin-top: -16px;
padding-bottom: 32px;
margin-top: calc( var( --space-4 ) * -1 );
padding-bottom: var( --space-8 );
text-align: center;
text-wrap: pretty;
}

:global([data-slot='empty']) > .body {
/* eslint-disable-next-line studio/no-primitive-overrides -- the prompt drops its dialog offsets inside an Empty, which spaces it already. */
:global( [data-slot='empty'] ) > .body {
margin-top: 0;
padding-bottom: 0;
}
Expand All @@ -19,7 +20,7 @@
width: 100%;
max-width: 448px;
aspect-ratio: 2.6 / 1;
margin-bottom: 20px;
margin-bottom: var( --space-5 );
pointer-events: none;
}

Expand All @@ -29,13 +30,14 @@
position: absolute;
top: 50%;
aspect-ratio: 4 / 3;
border-radius: var(--radius-md);
background-color: var(--muted);
border-radius: var( --radius-md );
background-color: var( --muted );
background-position: center;
background-size: cover;
/* eslint-disable-next-line studio/use-theme-tokens -- a one-off card shadow, lighter than --shadow-sm. */
box-shadow:
inset 0 0 0 1px var(--border),
0 1px 3px rgb(0 0 0 / 10%);
inset 0 0 0 1px var( --border ),
0 1px 3px rgb( 0 0 0 / 10% );
}

.cardStart,
Expand All @@ -45,64 +47,64 @@

.cardStart {
left: 10%;
transform: translateY(-45%) rotate(-8deg);
transform: translateY( -45% ) rotate( -8deg );
}

.cardEnd {
right: 10%;
transform: translateY(-45%) rotate(8deg);
transform: translateY( -45% ) rotate( 8deg );
}

.cardCenter {
left: 50%;
z-index: 1;
width: 43%;
transform: translate(-50%, -50%);
transform: translate( -50%, -50% );
}

@media (prefers-reduced-motion: no-preference) {
@media ( prefers-reduced-motion: no-preference ) {
.cardStart,
.cardEnd {
transition: transform calc(var(--motion-duration-layout) * 2) var(--motion-ease-enter);
transition: transform calc( var( --motion-duration-layout ) * 2 ) var( --motion-ease-enter );
}

@starting-style {
.cardStart {
transform: translate(62.5%, -50%) scale(0.97);
transform: translate( 62.5%, -50% ) scale( 0.97 );
}

.cardEnd {
transform: translate(-62.5%, -50%) scale(0.97);
transform: translate( -62.5%, -50% ) scale( 0.97 );
}
}
}

.title {
font-size: var(--text-xl);
font-size: var( --text-xl );
font-weight: 550;
line-height: 1.4;
letter-spacing: -0.02em;
}

.description {
max-width: 320px;
margin: 4px 0 0;
color: var(--muted-foreground);
font-size: var(--text-sm);
line-height: 1.625;
margin: var( --space-1 ) 0 0;
color: var( --muted-foreground );
font-size: var( --text-sm );
line-height: var( --leading-relaxed );
}

.error {
margin: 12px 0 0;
color: var(--destructive);
font-size: var(--text-xs);
margin: var( --space-3 ) 0 0;
color: var( --destructive );
font-size: var( --text-xs );
}

.createAccount {
margin-top: 28px;
margin-top: var( --space-7 );
}

.logIn {
margin-top: 16px;
font-size: var(--text-xs);
margin-top: var( --space-4 );
font-size: var( --text-xs );
}
20 changes: 10 additions & 10 deletions apps/ui/src/features/account/auth-actions/style.module.css
Original file line number Diff line number Diff line change
@@ -1,33 +1,33 @@
.root {
display: flex;
flex-direction: column;
gap: 12px;
gap: var( --space-3 );
}

.actions {
display: flex;
align-items: center;
gap: 16px;
gap: var( --space-4 );
}

.arrow {
margin-inline-start: 4px;
margin-inline-start: var( --space-1 );
display: inline-block;
}

[dir='rtl'] .arrow {
transform: scaleX(-1);
transform: scaleX( -1 );
}

.error {
padding: 10px 12px;
border: 1px solid color-mix(in oklab, var(--destructive) 35%, transparent);
border-radius: 6px;
background: var(--destructive-surface);
color: var(--destructive);
padding: var( --space-2_5 ) var( --space-3 );
border: 1px solid color-mix( in oklab, var( --destructive ) 35%, transparent );
border-radius: var( --radius-lg );
background: var( --destructive-surface );
color: var( --destructive );
}

@media (max-width: 600px) {
@media ( width < 640px ) {
.actions {
align-items: stretch;
flex-direction: column;
Expand Down
8 changes: 4 additions & 4 deletions apps/ui/src/features/account/gravatar/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
display: inline-flex;
width: 20px;
height: 20px;
border-radius: 50%;
border-radius: var( --radius-full );
flex-shrink: 0;
background-color: var(--chat);
background-color: var( --chat );
}

/* A hairline ring over the image, blended so it darkens light edges and
Expand All @@ -14,12 +14,12 @@
content: '';
position: absolute;
inset: 0;
border: 1px solid var(--border);
border: 1px solid var( --border );
border-radius: inherit;
mix-blend-mode: darken;
}

:root[data-color-scheme='dark'] .root::after {
[data-color-scheme='dark'] .root::after {
mix-blend-mode: lighten;
}

Expand Down
29 changes: 12 additions & 17 deletions apps/ui/src/features/account/user-menu/style.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
notification bell at the end, as 36px controls inset 12px. Pinning to the
sidebar bottom is handled by the layout's `sidebarFooter` wrapper. */
.root {
padding: 0 12px 10px;
padding: 0 var( --space-3 ) var( --space-2_5 );
}

.alert {
margin-block-end: 8px;
margin-block-end: var( --space-2 );
}

.row {
Expand All @@ -16,7 +16,7 @@

.offline {
display: flex;
color: var(--muted-foreground);
color: var( --muted-foreground );
}

.avatar {
Expand All @@ -35,38 +35,33 @@
.account {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 6px;
gap: var( --space-1_5 );
padding: var( --space-2 ) var( --space-1_5 );
}

.accountText {
display: flex;
flex-direction: column;
min-width: 0;
font-size: var(--text-xs);
line-height: 1.25;
}

.accountName,
.accountEmail {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: var( --text-xs );
line-height: var( --leading-tight );
}

.accountName {
font-weight: var(--font-weight-medium);
composes: truncate from '@/lib/theme/recipes.module.css';
font-weight: var( --font-weight-medium );
}

.accountEmail {
color: var(--muted-foreground);
composes: truncate from '@/lib/theme/recipes.module.css';
color: var( --muted-foreground );
}

.itemLabel {
flex: 1;
}

.shortcut {
color: var(--muted-foreground);
color: var( --muted-foreground );
font: inherit;
}
Loading
Loading