feat(ads): refresh installed design-system tokens, layers, and theming - #527
Merged
Merged
Conversation
Rename every ADS design token from a StyleX-hashed `defineVars` key to an explicit CSS custom property name, so `vars.colorText` becomes `vars["--ads-color-text"]` and the emitted variable is `--ads-color-text` instead of a hash. This is the prerequisite for compiling ADS and product code under different StyleX class name prefixes: a prefix renames generated CSS variables as well as classes, so a product-origin file would otherwise reference a token variable that only the ADS-origin sheet defines. Re-sync `tokens.stylex.ts` and `themes.stylex.ts` from the current ADS source in the same change, keeping this copy's Pretendard Variable sans stack, and install the `breakpoints.stylex.ts` module that upstream split out of `tokens.stylex.ts`. `AppShell.shell.styles.ts` imports breakpoints from the defining module because a StyleX condition key cannot resolve through the re-export. Verified: typecheck clean, `bun run test:isolated` passes 607 files, and the built stylesheet carries 1651 `--ads-*` references with no hashed token definitions and its media queries intact.
… layers A StyleX atomic class is a pure function of its declaration, so an ADS component and an app file that write the same declaration produced the same class and therefore one cascade position. That left no seam between them: a brand recipe placed below the band could not restyle a component, and placed above it overruled the product's own call sites. It also made a host style handed to an ADS component through `className` win or lose per property, decided by whichever module first emitted the atomic class. Replace `stylex.vite()` with `adsStylex()` in both Vite configs. ADS modules keep StyleX's default `x` class name prefix, everything else compiles under `p`, and each origin's rules are emitted as `@layer ads.priority*` / `@layer product.priority*` nested inside the positioned `ads` and `product` layers. The canonical order becomes `reset, theme, base, ads, ads-theme, product, components, utilities`, stated identically in `index.html`, `src/globals.css` and the vendored `styles.css`. `ads-theme` ships empty; it is the seam a brand recipe would occupy. `src/main.tsx` no longer imports `virtual:stylex:runtime`. That module belongs to the Vite adapter this plugin replaces, and `adsStylex` injects its own dev stylesheet and client; left in place it is an unresolvable import that makes the dev server return 500 on the entry module. `check:design-system` and the globals stylesheet test now assert the three origin layer names in order rather than matching a hardcoded `priority1`, so losing the seam fails instead of passing on two identical strings. Verified in a browser on the dev server: the document registers `reset, theme, base, ads, ads-theme, product, components, utilities`, and on a rendered ADS Button whose own `padding-inline` is 12px, an `ads-theme` rule renders 33px while the same rule alongside a product-layer rule renders 41px. No media query was lost (208 to 212 nested rules; the four added are declarations that now correctly hold one class per origin), and every `--ads-*` token resolves.
`className` on an ADS component is an opaque string channel: the component's
own atomic class and the host's both exist, and the winner is settled by
cascade position rather than at the call site. Splitting StyleX by origin made
that outcome uniform in the host's favour, but it is still invisible where it
is written, and it was previously decided per property — two properties in one
style object could go opposite ways.
`xstyle` is merged last into the part's own `stylex.props()` call, so it
resolves by property name in JavaScript before the cascade is consulted, and
reads the same before and after the origin split.
Add the prop to the vendored parts that lacked it — `Badge`, `Checkbox`, the
six `Card` parts, nine `Dialog` parts, five `Command` parts, two `Select`
parts — and move all 52 product call sites off `className`. `className` stays
where it carries a behaviour hook, a motion class, a test id, or a caller's
forwarded class; those sites keep it and gain `xstyle` alongside.
`Checkbox` routes `xstyle` exactly the way it already routes `className`: to
the control root in `controlOnly` mode, where the control is the outer box the
host sees, and to the label row otherwise. All three migrated Checkbox call
sites are `controlOnly`, so applying it only to the label row would have
dropped their styling.
Add `check:style-channel`, retargeted from the ADS original for a source
install: it scans `src/` minus the vendored directory and resolves each import
specifier against the importing file rather than matching a package name.
Wired into `test:ci`. Verified it fails on a reintroduced `className={sx(...)}`
and passes on the migrated tree.
Verified: typecheck, build, build:desktop, `test:isolated` (608 files) and every
repository gate pass; the composer and the ADS regression preview render in a
browser with no collapsed control and glyphs still on the 14/16px icon ramp.
Install the ADS `theming/` module and `tokens/theme-values.ts`, then re-sync the 27 vendored components this host has never modified. An audit reconstructed the pristine install baseline for 119 of the 120 manifest entries out of both repositories' object stores; for these 27 the current content is byte-identical to that baseline, so there is no host customization to preserve and the upstream file can be taken whole. They now emit stable theme targets — an `ads-<target>` class plus `data-variant`, `data-size`, `data-tone`, `data-density` and `data-ads-slot` — which is the addressing scheme a brand recipe needs. Nothing styles them yet and the `ads-theme` cascade layer reserved earlier is still empty. Stave's own theme system stays the single theme authority. It is user-installable at runtime as validated JSON, which a build-time CSS generator cannot express, so `defineProductTheme` and `ProductThemeProvider` are installed but unmounted. That forced one host modification. `PortalProductThemeScope` upstream re-applies the mode and density token groups on the portal wrapper, because upstream supports a provider scoped to a region. Here the single root provider already mirrors those groups onto the document under `syncDocument`, and `StaveDesignProvider` publishes the host palette mapping over them as inline custom properties on `<html>` — so a class on the closer portal wrapper would have replaced the user's saved theme with the design system's stock palette inside every dialog, menu, popover, drawer, select and tooltip. The groups now apply only when a brand is actually scoped; attributes, `color-scheme` and the selection pair still apply unconditionally because they declare no token. `Select.parts.tsx` takes a partial forward-port of `readOnly` only, because the re-synced `Select.array.tsx` passes it; the rest of that file awaits its merge. `Calendar.keyboard.ts` keeps a strict-indexed-access adaptation this repo needs and upstream does not. Verified: every repository gate, `test:isolated` (609 files), both builds, and in a browser that a portalled surface still resolves the host palette (`--ads-color-accent` reads the theme's `--primary`, not the ADS default). `tests/ads-portal-theme-scope.test.tsx` fails if the upstream unconditional form is restored.
Three-way merge `utils/stylex.ts`, `recipes/transition.ts`, `recipes/menu.ts` and the Button family against the reconstructed pristine install baseline. Upstream had independently adopted three of this host's extensions verbatim — the `bar`/`slide` transition recipes, the native-button reset on `menu.item`, and Button's indicator slot styles — so those collapse to upstream's spelling. The host modifications that survive: the exported, widened `StyleXValue` and unfiltered `sx` that `xstyle` arrays depend on, Button's `layout="host"` render path, `aria-busy` honouring a caller-passed value, and `xstyle` merged ahead of the disabled and inert expressions so a disabled control's `cursor` and `opacity` stay authoritative. Two upstream removals reach product code. `Button` now publishes its theme identity on `data-variant`/`data-size`/`data-tone`, spread after the caller's props, so the wrapper's own duplicates were being overwritten before they reached the DOM and are removed; the element now carries the design system's spelling. And `buttonDangerToneStyles` is gone — semantic tones are one implementation driven by `--ads-button-tone-*` custom properties applied as an inline style, so the class-only `buttonVariants()` path, which has no element to carry one, declares the same five properties through StyleX instead. That last one crosses the origin split, so it was measured rather than assumed: in the live sheet a product-origin class declares `--ads-button-tone-fill` and an ADS-origin class consumes it for `background-color`, and an element carrying both paints this theme's `--destructive`. Verified: every repository gate, `test:isolated` (609 files), both builds.
Three-way merge Badge, Card, Table, Breadcrumb, EmptyState, Plan, StepRail, DiffViewer, Command and the ToolRun style/parts modules against the pristine install baseline, and install the siblings they now import: `Command.types`, `Command.dialog`, `IconTile`, `truncating-label`, `recipes/value-token` and `recipes/overlay-surface`. Most host extensions in this group had already been adopted upstream verbatim — `xstyle` across every part, EmptyState's centred header, Plan's `space8` header inset, ToolRun's 22rem container arm, and DiffViewer's whole host syntax highlighter seam — so those hunks collapse to upstream's spelling rather than being re-applied. What survives as host bytes is the canonical style exports the product's compound adapters import, and ToolRun's three deltas. `ToolRun.parts.tsx` was hand-merged with no base: its recorded integrity matches no blob in either repository, because the bytes installed were an uncommitted upstream working-tree state. It starts from upstream with the three known host deltas re-applied and nothing else, confirmed by diff. `Command.styles.ts` is taken from upstream even though upstream never moved it, because upstream's `Command.tsx` uses three keys this copy had deleted as unused. The host's own additions are superseded: the backdrop and popup surface are now `recipes/overlay-surface`, declaration for declaration, and the keycap restyle is now a real `Kbd` inside a positioning-only wrapper. Two product call sites follow intentional upstream removals. `badgeStyles.root` and `.outlineBase` moved to `recipes/status-chip` with a new size axis, so the class-only `badgeVariants()` composes the same three keys the component does. `emptyStateStyles.media` and `.toneNeutral` are gone — the medallion is an `IconTile` — so `EmptyMedia`'s icon arm delegates to the ADS part, which also gives it upstream's hairline rim. Verified: every repository gate, `test:isolated` (609 files), both builds. The `EmptyMedia` tag change is layout-inert: all 26 call sites are direct children of the empty header, which is a grid, so an inline child is blockified by spec.
Three-way merge Dialog, Drawer, Popover, Menu, Tooltip, ToastHost and `overlay-motion.css` against the pristine install baseline, and install the siblings they now import: `Dialog.styles`, `Menu.styles`, `Menu.density`, `Menu.merge-class-name`, `Tooltip.group` and `ToastHost.styles`. Upstream had independently adopted every behavioural host modification in this group. The `surface`/`popup` split this copy carried is now expressed as `recipes/overlay-surface` plus a geometry-only `popup`, declaration for declaration; the flush popover density generalised from `PopoverDensity` to a three-value `OverlayContentDensity` covering the same need; and the ToastHost column restructure — the largest host change in the install — turned out to be byte-identical to upstream's new `ToastHost.styles.ts`, comments included. What survives as host bytes is four style exports the product's compound adapters import, and ToastHost's `toastManager` passthrough. Every file here portals, so all of them now wrap portalled content in `PortalProductThemeScope`. That module keeps its host modification: it re-applies the mode and density token groups only when a brand is actually scoped, because this host already mirrors them onto the document and publishes its own palette mapping over them. Measured in a browser on a real open popup — the scope wrapper carries the `contents`, `nativeChrome` and `colorScheme` classes but not the mode theme class, generates no box, and the popup resolves `--ads-color-accent` to the theme's `--primary`, identical to the document. Seven product call sites follow the one forced API change: the `surface` key no longer exists on these style objects, so they compose the same `recipes/overlay-surface` keys the components do. The premise that menu shims would lose their `data-variant` did not hold: the `menu-item` theme target publishes `density` and `tone` only, so the product attribute still reaches the DOM. It is now redundant rather than overwritten and was left in place, since no upstream change forces its removal. Verified: every repository gate, `test:isolated` (609 files), both builds.
Three-way merge TextField, Textarea, Switch, Toggle, Slider, Select.parts, Tabs,
Tabs.styles and Calendar against the pristine install baseline, and install the
siblings they now import: `Calendar.styles`, `date-range`, `Tabs.session` and
`Tabs.session.styles`.
Upstream had adopted the Tabs `xs` rung whole — the caption type step, the
compact row height with its coarse-pointer floor, `root` align-content and the
line-strip stretch alignment — and moved `TabsSize` into `Tabs.styles.ts`, so the
host version collapses to upstream's. `Select.parts.tsx`'s partial `readOnly`
forward-port from an earlier pass likewise became upstream's own code.
Three host modifications survive because upstream still overwrites where this
copy merges: TextField, Textarea and Switch each compose a caller's
`aria-describedby` with the field anatomy's rather than replacing it, and OR the
caller's `aria-invalid` into the anatomy's. That is accessibility behaviour, not
styling, so it is re-applied on upstream's structure. Calendar keeps its
strict-indexed-access adaptations, one of which moved with `parseDateKey` into
the new `date-range` module.
`merge3.py` reported two files MERGED CLEAN that were not: both sides had added
`xstyle` and the `xs` rung at the same anchor, producing duplicate bindings and a
duplicate `const`. Both were reset to upstream with the host delta re-applied.
`ui/select.tsx` drops a now-overwritten `data-size`: the `select-trigger` target
declares a `size` axis and `{...theme}` is spread after `{...props}`.
`ui/switch.tsx` keeps its `data-size` — the `switch` target declares only a
`tone` axis, so the attribute is untouched, not shadowed.
Verified: every repository gate, `test:isolated` (609 files), both builds, and
all three ARIA merges confirmed by rendering.
Three-way merge `ThemeProvider.tsx`, `styles.css` and `fonts.css`, completing the
re-sync. `ThemeProvider` is now byte-identical to upstream — both remaining
conflicts were prose, because upstream had already adopted the non-destructive
`<html>` restore and the selection token pair this copy carried.
`styles.css` takes upstream's zero-specificity reset: every selector wrapped in
`:where()` at 0,0,0, so a host rule placed in the same layer wins by weight
rather than by whichever sheet the bundler parsed last. Three host rules stay —
the layer statement's comment, the `layout="host"` carve-out on the control
glyph rule, and the `@layer base` default that gives a host-layout Button's glyph
a size without mandating one. `--atelier-motion-tooltip-delay` goes with
upstream's move of the tooltip dwell to a JS rest timer, which is a perceptible
timing change to every tooltip.
`fonts.css` keeps this copy's Pretendard variable dynamic subset — one variable
face across 92 unicode-range slices instead of four static weights — and adopts
the `layer(ads)` annotations, inert for `@font-face` but keeping the sheet in the
package's own origin.
All three manifests now record `sourceRevision`. Every file's integrity is the
hash of the upstream file at that commit, so the merge base for the next upgrade
is one `git show` away and is verifiable against the recorded hash. This
migration had no such anchor: the baseline had to be recovered by scanning both
repositories' object stores for a blob matching each recorded hash, and one file
matched nothing because its recorded bytes were never committed upstream.
`tests/ads-control-chrome.test.ts` now asserts the zero-specificity form and
fails on any unwrapped reset selector, rather than merely checking an element
name appears. Its region slice was also anchored on the `@layer base {` block
instead of the bare phrase, which upstream's new prose had begun matching.
Verified: every repository gate, `test:isolated` (609 files), both builds.
The previous commit advanced every manifest's integrity to ADS `589cfc4c`, but this migration only re-synced the closure `.ads-source.json` tracks. 21 files belong exclusively to the control and lightbox manifests — AppShell and its sidebar parts, Lightbox, Checkbox, RadioGroup, NativeSelect, CappedViewport, Citation, Thinking, the ToolRun group and the small agent-surface parts — and they still sit at the upstream content they were installed from plus their host modifications. Recording `589cfc4c` as their baseline would claim a merge base that was never merged: the next upgrade would diff against the wrong side and silently drop host work. Those entries go back to their original install hashes, and the two sibling manifests no longer claim a `sourceRevision` at all, since one is only meaningful when every file it covers was resolved against it. Files shared with `.ads-source.json` were genuinely re-synced and keep the advanced baseline. Verified: no sibling-exclusive baseline is advanced, every re-synced entry's baseline equals upstream at the recorded revision, and typecheck, `test:isolated` (609 files) and the design-system gate pass.
astyfx
requested review from
heath-s and
png-1105
and removed request for
a team
September 14, 2026 13:39
astyfx
enabled auto-merge (squash)
September 14, 2026 13:40
|
| Commit | Scanned at | New | Resolved | Net |
|---|---|---|---|---|
7696940 < |
2026-09-14 13:40 UTC | 0 | 0 | 0 |
Last scanned: 7696940 · 2026-09-14 13:40 UTC
|
| Commit | Scanned at | New | Resolved | Net |
|---|---|---|---|---|
7696940 |
2026-09-14 13:40 UTC | — | — | — |
7696940 < |
2026-09-14 13:40 UTC | 0 | 0 | 0 |
Last scanned: 7696940 · 2026-09-14 13:40 UTC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Refresh the installed design-system copy to the current ADS source (
589cfc4c): explicit--ads-*token names, StyleX compiled per origin so design-system and product rules occupy separate cascade layers, thexstylehost style channel, theme targets, and a three-way merge of the vendored components this host already customized.Changes
Features
xvsp) so design-system, brand, and product rules occupyads/ads-theme/productlayersxstylehost style channel and acheck:style-channelCI guardads-<target>plusdata-variant/data-size/data-tone/data-density) so a brand recipe has a stable addressFixes
PortalProductThemeScopefrom re-applying mode/density token groups on every portal, which would replace the host's runtime palette inside dialogs, menus, popovers, drawers, selects, and tooltipsRefactors / Chores
--ads-*custom properties so origin prefixes cannot break token referencessourceRevisionon the primary manifest so the next upgrade has a verifiable merge baseBreaking Changes
xstyle, notclassName;check:style-channelfails the lattersurfacekey — composerecipes/overlay-surfaceinsteadbadgeStyles.root/.outlineBasemoved torecipes/status-chip; empty-state icon media is now anIconTileTest Evidence
Local, this session:
bun run typecheckpassedbun run typecheck:lens-mainpassedcheck:reliability-gates,check:doc-paths,check:max-lines-ratchet,check:switch-exhaustiveness,check:style-channel,check:design-systempassedLocal, prior to this publication (clean tree, no further edits):
bun run test:isolatedpassed (609 files)bun run buildandbun run build:desktoppassedads-themerule beats an ADS Button's ownpadding-inline; a product-layer rule still wins overads-theme; a live button with both origin classes resolves--ads-button-tone-fillto the theme's--destructive; an open popup's portal wrapper does not carry the mode theme class and--ads-color-accentstill reads the host--primaryNot run here:
check:licenses— this worktree has nolicense-checkerpackage undernode_modules. CI installs from the lockfile. No dependency was added.Notes
Stave runtime themes remain the theme authority.
defineProductTheme/ProductThemeProviderare installed but not mounted: user-installable JSON themes cannot be expressed as a build-time CSS generator.ads-themeships empty.21 files that belong only to the control and lightbox manifests (AppShell and sidebar parts, Lightbox, Checkbox, RadioGroup, NativeSelect, CappedViewport, and the agent-surface / ToolRun / Thinking / Citation group) were not re-synced. Their recorded integrity stays at the original install baseline.
Perceptible visual changes from upstream: tooltip dwell moved from a CSS variable to a JS rest timer;
EmptyMedia variant="icon"now rendersIconTile's hairline rim; warning/success hues were retuned with the token re-sync.Host modifications that survive the merge are listed in
src/components/ads/PROVENANCE.md(157 of 212 installed files are byte-identical to ADS589cfc4c).