Skip to content

refactor!: rebuild the component library on Base UI + Tailwind v4 for UI3 - #1

Merged
eteplus merged 162 commits into
mainfrom
refactor/tailwind-base-ui
Oct 30, 2026
Merged

eteplus merged 162 commits into
mainfrom
refactor/tailwind-base-ui

Conversation

@eteplus

@eteplus eteplus commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Lands the full 0.1.0 rework: the library is rebuilt on Base UI primitives and Tailwind CSS v4, with every component audited against the official UI3 Design file.

Styling & tokens (breaking)

  • UnoCSS is removed; the theme ships Tailwind v4 CSS generated from token JSON (@thinkbuff/figma-theme/css/*, @thinkbuff/figma-react/styles.css).
  • Tokens are generated from the Design file variable export (pnpm tokens:import) and support all eight editor theme modes (Figma/FigJam/Dev Mode/Slides × light/dark).
  • UI3 typography and elevation tokens added; spacing renumbered to the UI3 scale; published semantic colors grow from 268 to 490 per mode.

Primitives (breaking)

  • All components migrate from Radix UI to Base UI (render prop, aria-expanded open-state styling); Radix dependency removed.
  • Requires React 19.

Component alignment with the UI3 Design file

  • Every component audited per-component set: Button, ActionIcon/Toggle, menus, fields, Slider/Swatch, Avatar/Badge, Radio/Tabs/Toast/VisualBell/ScrollArea.
  • Button API reworked to a single variant prop + size="wide" + icon slot; ActionIcon gains variant; Toggle stroke → variant.
  • New components: Badge, Avatar/AvatarGroup, VisualBell, Swatch chit, VariableChip, LinkTooltip, ActionIconSplit, SegmentedControl (folded into ToggleGroup), ContextMenu, ValueField family, UI3 icon set via createIcon.

Toolchain & docs

  • Build moves to vite-plus/tsdown, lint/format to oxlint/oxfmt (vp check); vitest unit + browser tests; CI runs check/typecheck/tests/build.
  • Releases are automated: pushing a v* tag runs release.yml — tag-vs-version guard, full verify, npm publish via trusted publishing (OIDC + provenance), and an auto-created GitHub release.
  • pages.yml deploys the Storybook + playground site to GitHub Pages on every push to main (pnpm pages:build assembles site/figma/: redirect index + storybook + playground).
  • Architecture doc, GLOSSARY, ADRs and per-component migration reports added; UI3 Design file adopted as the single design reference.
  • Third-party attributions consolidated into NOTICE.md; published tarballs now ship LICENSE (and NOTICE.md for figma-react).

Test plan

  • pnpm test — 373 unit + browser tests
  • pnpm check / pnpm typecheck / pnpm build
  • pnpm test:visual — 620/620 pixel-exact baselines (threshold 0)
  • pnpm -F @thinkbuff/figma-react build-storybook
  • pnpm pack tarball verified — LICENSE/NOTICE.md included, no stale attribution comments in dist

Adopt pnpm catalogs, replace eslint with oxlint/oxfmt via vite-plus,
build packages with vp pack, and upgrade storybook to v10.
Port the platform Tailwind v4 theme into @thinkbuff/figma-theme, replace
UnoCSS utilities and variant groups with Tailwind classes, inline SVG
icons, and ship @thinkbuff/figma-react/styles.css with @source.

BREAKING CHANGE: @thinkbuff/figma-theme/unocss is removed; consumers use
Tailwind v4 and import @thinkbuff/figma-react/styles.css.
Drop all @radix-ui dependencies and the radix catalog now that every
component runs on Base UI, widen the react peer range to ^18.2.0 || ^19,
and document the breaking API changes in the changelog.
Replace figma-kit with the Design file as the reference for token values and
component appearance, and record the resulting architecture: layers, token
pipeline and naming, usage rules, directory conventions, extension points and
release policy. Add ADRs for runtime variable names, the normalized token
source and flat component directories. Drop the figma-kit based UI3 migration
notes; component audits now stay local.
Add `pnpm tokens:import <dir>`, which normalizes the Figma "Export modes" DTCG
files into packages/theme/tokens/ (eight color modes, sizing, typography).
Color maps and generated CSS now derive from that JSON instead of hand-copied
values; elevation and repo-only extensions are hand-maintained JSON.

All semantic Design file colors except the fullscreen, desktop, code and
inspect families are published (490 per mode, up from 268). `:root` now
resolves Figma light: the FigJam block also claimed it and won the cascade,
so unthemed pages rendered FigJam colors. The shared/mode-invariant split is
gone, so `BaseThemeCSSVariables` is removed and every mode map is complete.
Generate a block per mode: the editor-type class (`figjam`, `devmode`,
`slides`) combines with the runtime `figma-light`/`figma-dark` class, so FigJam
dark, Dev Mode and Slides resolve their own colors and elevation, including in
nested containers. Standalone `figjam.css`, `devmode.css` and `slides.css`
entries resolve their editor's light mode at `:root` again.

Drop the legacy `xs/sm/md/lg` type scale in favour of the Design file styles
(`text-figma-body-medium`, `text-figma-body-large`), and order elevation shadow
layers as in the Design file. Storybook gains all eight themes; visual tests
add FigJam dark baselines. Existing baselines are pixel-identical.
The pixelmatch threshold of 0.2 let light-gray color changes and sub-pixel
text shifts pass, so baselines drifted from the rendered UI (e.g. the badge
restyle and the 0.5% body letter spacing never reached them). Use threshold 0
and re-record the 267 baselines that no longer matched; the strict suite is
deterministic across consecutive runs.
- Collapse variant+color into a single variant prop matching the Design
  Button component set: primary, secondary, destructive,
  secondary-destructive, inverse, success, figjam, link, link-danger,
  ghost.
- Secondary is now the outlined bordertranslucent treatment; ghost uses
  the transparent-secondary tokens; filled variants get the selected
  focus outline plus the white inset focus ring from the Design file.
- Replace fullWidth with size="wide" and add a 24px leading icon slot.
- Remove the warning variant and dead btn-* classes; migrate stories
  and overlay consumers to the new API.
- Register the new Button utilities in the theme utility contract.
- ActionIcon gains variant="secondary" (persistent bordertranslucent
  stroke per the Button icon set) and drops activeAppearance — the
  expanded state now always follows the dialog-toggle spec
  (bg-selected, bg-selected-secondary hover, no icon tint).
- Toggle replaces stroke with variant: "highlighted" (default),
  "default" (icon-swap, no pressed fill per Button icon toggle), and
  "secondary" (Dialog Toggle w/ Stroke; pressed drops the stroke).
- Hover/active fills switch to the Design file's transparent tokens
  (bghovertransparent, bgtransparentsecondary/hover); pressed hovers
  use bg-selected-secondary.
- Add border-figma-bordertranslucent to the theme utility contract.
New Core component for the UI3 `Button icon split` set: an
`ActionIconSplit` root (gap-px container, size small 41x24 / large
49x32, disabled propagated via context) with `Button` and `Chevron`
subparts. Halves carry the spec's per-side corner radius; hover/active
fills use the transparent tokens, focus draws the 1px selected stroke
on the focused half, and `aria-expanded` follows the dialog-toggle
fill so the chevron half can trigger menus via `render`.
- Row highlight uses bg-menu-selected (fixes the wrong #50297a deep
  purple in FigJam dark)
- ItemBadge uses bg-toolbar + text-onbrand per the spec's Badge
  small/Variant=Menu
- ItemCheck renders the spec's plain 16px check icon instead of a
  filled selected-strong chip
- Disabled rows unify on text-menu-disabled / icon-menu-disabled
- New ItemSwitch on DropdownMenu and ContextMenu for the spec's Menu
  row/Toggle: a menuitemcheckbox row with a leading Switch cell;
  clicking toggles without closing the menu
- Select.Trigger: single border-selected focus/open ring (drops the
  double outline), texthighlight block on the open value, new stroke
  prop for the borderless Stroke=False variant, flush 24px icon-lead
  cell and the spec's 24px bg-secondary well on size=large
- ValueField.NumericSelect: chevron segment uses bg-selected +
  border-selected while open
- VariableChip: 16px tall per spec; disabled uses border-disabled
- New VariableCell for the spec's Variable Cell row
- New bg-figma-texthighlight utility; Input gains a VariableChip
  composition story for the Variable state
Slider:
- new `modified` prop renders the spec's Stroke (Modified) thumb
  (bg-default knob, icon-onbrand ring, elevation-300)
- hue/opacity thumbs embed the spec's 12px _Chit 24 square (a small
  Swatch) instead of a 6px disc
- disabled thumb uses bg-disabled (was bg-disabled-secondary); track and
  gradient-chit rings use the bordertranslucent token; stepper ticks use
  icon-tertiary / icon

Swatch:
- new `type="opacity"` (left half solid, right half ~24% alpha) and
  `shape="circle"` (16px round chit, no ring) per the _Chit 24 spec
- inset ring binds to the bordertranslucent token
The handle spec embeds _Chit 24/Variant=Circle (a 12px round chit),
not a square one. Swatch's shape="circle" now honors `size` so the
thumb chit renders as 12px.
The thumb's 4px border shrinks its flex content box to 8px, so the
12px chit was flex-shrunk to 8x12 — an ellipse. `shrink-0` restores
the spec's round chit inside the ring.
The handle embeds a 12px _Chit 24 instance, but that component is a
24px cell whose visible circle is only 16px — so the dot inside the
16px thumb is 8px, not a 12px solid circle. Render a plain 8px dot.
Avatar:
- initials fills bind to the multiplayer* / textonmultiplayer* tokens
  via new semantic utilities (were hardcoded light-theme hexes — dark
  themes rendered the wrong colors)
- colors renamed to spec names (magenta->pink, slate->grey); new
  overflow-unread / overflow-read colors for "+N" avatars; new
  `disabled` prop (bg-tertiary + text); square at size="small" uses
  r=2 (rounded-figma-sm)

Badge:
- `size="small"` (14px) removed — no spec counterpart; `size="large"`
  added (16px, 13px text — "Badge large")
- new `variant="alt"` for the "Badge small alt" subtle-fill family
- new colors figjam, menu, merged, archived; warning strong uses the
  icon-warning token
- new BadgeDot (9px bg-default pill + 5px bg-brand dot)
… UI3 Design file

- Radio: button-variant border uses `bordertranslucent` per spec
- Tabs: `border-selected` focus ring; unselected hover uses `bg-hover`
- Toast: `bg-menu`/`text-menu` surface tokens and the spec's HUD shadow
  via new `shadow-figma-hud` extension token
- VisualBell: `color="default"` is the spec's light bell (`bg-default`
  + `text`, elevation-100); edges use `bordertranslucent`; hover/active
  use the secondary-button transparent tokens
- ScrollArea: thumb uses the `scrollbar` token via `bg-figma-scrollbar`
- Switch: indeterminate renders the spec's icon.16.mixed glyph instead
  of a hand-drawn bar
- Input/Textarea: disabled state follows the spec per size — default
  keeps bg-default + border-default; large stays bg-secondary bare
- Align disabled colors to spec tokens where values are identical:
  text-tertiary on Select, ToggleGroup, and ValueField; border-default
  on ValueField outlines
- LinkTooltip.Action: add `strong` prop for the spec's
  body.medium.strong CTA weight
- Separator: document per-surface divider tokens
- AlertDialog story: CTA gap aligned to the Modal footer's 8px rhythm
…tical

The range bleed only reached -6px while the track overhangs the value
domain by 8px, leaving a 2px gap at the min cap and a stray stub inside
the hollow thumb on empty ranges. Extend the min-side bleed to -8px and
skip it entirely when the range is empty.

Vertical sliders were broken end to end: group-data- on the Control could
never self-match so the track never became flex-col, and the
bottom-anchored indicator's py-1.5 -mt-1.5 painted [v1, v2+12] — 8px
short of the bottom cap and past the top thumb's ring. The indicator now
mirrors the horizontal geometry ([v1-8, v2+6]).
UI3 scrollable tab lists signal overflow through the fade mask, not a
visible scrollbar — hide it while keeping horizontal overflow intact.
Replace the Vite starter with a navigable showcase for the refactored component APIs.

Adds six sections, an eight-mode theme switcher, and the library ScrollArea.

Slider demos include 0% and min-edge range boundary cases plus a vertical group.
The playground resolves @thinkbuff/figma-react types through dist
artifacts, so vp check and tsc need the packages built first.
The eight-mode rework scopes figjam under .figjam/.figma-light
combinators instead of html.figjam; update the stale assertion.
@eteplus
eteplus merged commit d9a29bf into main Oct 30, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant