diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c28e8bd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-workspace.yaml') }} + - run: pnpm -F @thinkbuff/figma-react exec playwright install --with-deps chromium + - run: pnpm build + - run: pnpm check + - run: pnpm typecheck + - run: pnpm test + - name: generated theme css is committed + run: git diff --exit-code -- packages/theme/src/css/ diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..b9a1919 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,37 @@ +name: pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + deploy: + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm pages:build + - uses: actions/configure-pages@v6 + - uses: actions/upload-pages-artifact@v5 + with: + path: site/figma + - id: deployment + uses: actions/deploy-pages@v5 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f61c198 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,52 @@ +name: release + +on: + push: + tags: ['v*'] + +permissions: + contents: write # create the GitHub release + id-token: write # npm provenance / trusted publishing + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: pnpm/action-setup@v6 + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: pnpm + + # A mistyped tag publishes the wrong version; refuse early. + - name: tag matches package version + run: | + pkg=$(node -p "require('./package.json').version") + [ "$GITHUB_REF_NAME" = "v$pkg" ] || { + echo "::error::tag $GITHUB_REF_NAME does not match package.json version v$pkg" + exit 1 + } + + - run: pnpm install --frozen-lockfile + - uses: actions/cache@v6 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('pnpm-workspace.yaml') }} + - run: pnpm -F @thinkbuff/figma-react exec playwright install --with-deps chromium + - run: pnpm check + - run: pnpm typecheck + - run: pnpm test + - run: pnpm build + + # Auth is npm trusted publishing (OIDC) — no token needed. + - name: publish to npm + run: pnpm -r publish --provenance --no-git-checks + + - name: create github release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ver="${GITHUB_REF_NAME#v}" + awk -v v="$ver" '$0=="## v"v{f=1;next} f&&/^## /{exit} f' CHANGELOG.md > notes.md + gh release create "$GITHUB_REF_NAME" --notes-file notes.md --verify-tag diff --git a/.gitignore b/.gitignore index c0a6bd3..298216b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,17 @@ coverage # parcel-bundler cache (https://parceljs.org/) .cache + +# storybook static build output +storybook-static + +# assembled github pages site (scripts/pages.ts) +site/ +.vitest + +# Devin local config +.devin/*.local.json + +# Local Design file working-copy links and component audits +docs/figma-ui3-design.md +docs/audit/ diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..e34b6d8 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,8 @@ +{ + "recommendations": [ + "vitest.explorer", + "oxc.oxc-vscode", + "antfu.pnpm-catalog-lens", + "bradlc.vscode-tailwindcss" + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 350aa46..7aa1034 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,32 +1,32 @@ { - "editor.formatOnSave": false, + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "editor.defaultFormatter": "oxc.oxc-vscode", "editor.codeActionsOnSave": { - "source.fixAll.eslint": "explicit", + "source.fixAll.oxc": "explicit", "source.organizeImports": "never" }, - "eslint.useFlatConfig": true, - "eslint.workingDirectories": [ - { - "pattern": "./apps/*/" - }, - { - "pattern": "./packages/*/" - } - ], - "eslint.validate": [ - "javascript", - "javascriptreact", - "typescript", - "typescriptreact" - ], - "typescript.tsdk": "node_modules/typescript/lib", - "cSpell.words": [ - "onbrand", - "oncomponent", - "ondanger", - "ondisabled", - "oninverse", - "onselected", - "onwarning" - ] + "editor.quickSuggestions": { + "strings": "on" + }, + "[typescript]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[typescriptreact]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "[yaml]": { + "editor.defaultFormatter": "oxc.oxc-vscode" + }, + "explorer.fileNesting.enabled": true, + "explorer.fileNesting.patterns": { + "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .oxfmt*, oxfmt*, .prettier*, prettier*, .editorconfig", + "tsconfig.json": "tsconfig.*.json, env.d.ts, *.tsbuildinfo", + "vite.config.*": "jsconfig*, vitest.config.*, playwright.config.*" + }, + "files.associations": { + "*.css": "tailwindcss" + }, + "tailwindCSS.classAttributes": ["class", "className"], + "tailwindCSS.classFunctions": ["cn", "cva", "clsx", "addClassName"] } diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d7a107a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,90 @@ +# AGENTS.md + +Component library for Figma/FigJam plugin UIs: `packages/react` (React + Base UI ++ Tailwind v4) over shared tokens in `packages/theme`. `playground/` is a +consumer smoke test. Layering, token rules, naming and release policy: +`docs/architecture.md`; decisions in `docs/adr/`; vocabulary in `GLOSSARY.md`. +Per-component design audits live in `docs/audit/` (gitignored, local only). + +## Layout + +- Tests live in `packages//tests/` mirroring `src/` (e.g. + `src/components/x/x.test.tsx` → `tests/components/x/x.test.tsx`); shared + helpers in `packages/react/tests/test-utils/`; never colocate tests in `src/`. +- React tests import sources via the `~` alias (`~/components/x/x`); theme and + utils tests use relative `../src/...` paths. + +## Verify + +- `pnpm test` — unit + browser tests. +- `pnpm test:visual` — pixel-exact screenshot suite (207 baselines). CI does not + run it; always run locally before finishing UI work. +- `pnpm typecheck`, `pnpm check`, `pnpm build`. +- `pnpm deps` — taze interactive dependency updates (`-r -I -l`). +- `pnpm -F @thinkbuff/figma-react build-storybook` after component changes. + +## Visual tests + +- Baselines: `packages/react/tests/visual/__screenshots__/*-chromium-darwin.png`. + `pnpm test:visual:update` rewrites them — inspect every changed image before + committing; an update bakes whatever rendered, including bugs. +- Vite can serve stale styles: a "no diff" result may be a cached render, not a + real match. The vrt scripts clear `node_modules/.vite` first. To verify a + suspicious result, dump computed styles: + `VRT_PROBE="" pnpm -F @thinkbuff/figma-react probe`. +- `pnpm vrt:diff -- [vitest args]` snapshots baselines, re-runs the update and + lists which shots changed (before-copies under `$TMPDIR/figma-vrt-diff/`). +- An update run can rewrite PNGs byte-differently with identical pixels; compare + decoded pixels against `HEAD` with PIL + `ImageChops.difference(a.convert('RGB'), b.convert('RGB')).getbbox()` (RGBA + `getbbox()` only checks alpha) and `git restore` the pixel-identical ones + before committing. +- The comparator is pixelmatch with `threshold: 0` — strict, any changed pixel + counts. + +## Theme (`packages/theme`) + +- Token values live only in `tokens/*.json`. `tokens/colors/*`, `sizing.json` + and `typography.json` come from `pnpm tokens:import ` (a Figma "Export + modes" folder; copy it out of `~/Downloads` first — the sandbox can't read + it); `elevation.json` and `extensions.json` are hand-maintained. Never edit + imported files by hand unless the Design file can't be exported. +- `src/css/**` is generated by `pnpm generate:css` — edit `tokens/` or + `src/theme/`, never the output. CI fails on uncommitted drift. +- Styles resolve through built artifacts: `styles.css` imports the theme's + `dist/css` and scans `packages/react/dist` via `@source`. After adding a + utility or changing classes, rebuild (`pnpm build`) or VRT/Storybook renders + stale CSS with silently missing rules. +- `--figma-color-*` values are injected by the Figma editor at runtime + (`themeColors: true`); see `docs/figma-runtime-colors.md` to extract them. +- Register every new utility family in `packages/react/src/utils/cn.ts` + (tailwind-merge groups) or `cn()` merges it into the wrong group. +- Elevation/shadow variables live in the per-theme blocks (`.figma-light`, + `.figma-dark`, `.figjam`), not `@theme`, so nested `.figma-dark` resolves dark + values. +- Design reference: the UI3 Design file (official community file linked in + `docs/architecture.md` §1). Match component sets by page and set name, never + by node id; working-copy links live in the gitignored + `docs/figma-ui3-design.md`. + +## Components (`packages/react`) + +- Base UI conventions: `render` prop (never `asChild`), `aria-expanded` for + open-state styling, `addClassName` for callback classNames, `cn()` to merge. +- Controlled `Tabs.Root value` scrolls the tab list to the end on mount (Base UI + composite scrolls the active item into view). Prefer `defaultValue` unless + external state control is required. +- Prefer canonical Tailwind forms (`data-popup-open`, `inset-0.5`, + `-top-1.25`) over bracket variants and arbitrary values. +- Component styles are cva variants in `-variants.ts` (or inline classes) + built only from Utility contract classes; `packages/react/styles.css` + `@layer components` is only for what cva cannot express (keyframes, + third-party DOM). + +## Commits + +- Conventional Commits (commitlint enforced); pre-commit runs `vp check --fix` + on staged files. +- File names are kebab-case (`unicorn/filename-case` is an error); conventional + uppercase files like `README.md` and `AGENTS.md` are the exception. +- Do not kill listeners on dev ports (Storybook uses 6006) you did not start. diff --git a/CHANGELOG.md b/CHANGELOG.md index fd8d9f2..b0dad18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,133 @@ # Changelog +## v0.1.0 + +### ⚠️ Breaking Changes + +#### Styling: UnoCSS → Tailwind CSS v4 + +- `@thinkbuff/figma-theme/unocss` is removed. The theme now ships Tailwind v4 CSS (`@thinkbuff/figma-theme/css/*`) generated from the Figma tokens; `@thinkbuff/figma-theme/colors` is unchanged. +- `@thinkbuff/figma-react` ships `styles.css`, so consumers only need: + ```css + @import 'tailwindcss'; + @import '@thinkbuff/figma-react/styles.css'; + ``` +- Utilities follow the theme's utility contract (`text-figma-sm`, `rounded-figma-sm`, `bg-figma-*`, …). Dark mode is `.figma-dark`; FigJam is `.figjam`. +- Components use inline SVG icons, so no icon preset is needed. + +#### `Button` API rework + +- `variant` + `color` collapsed into a single `variant`: `primary`, `secondary`, `destructive`, `secondary-destructive`, `inverse`, `success`, `figjam`, `link`, `link-danger`, `ghost`. `size="wide"` replaces `fullWidth`; new `icon` slot prop. + +| Before | After | +| --- | --- | +| `variant="solid" color="primary"` (default) | `variant="primary"` | +| `variant="solid" color="danger"` | `variant="destructive"` | +| `variant="solid" color="success"` | `variant="success"` | +| `variant="solid" color="inverse"` | `variant="inverse"` | +| `variant="outline"` / `color="secondary"` | `variant="secondary"` | +| `variant="outline" color="danger"` | `variant="secondary-destructive"` | +| `variant="link" color="danger"` | `variant="link-danger"` | +| `variant="link"` | `variant="link"` | +| `variant="ghost"` | `variant="ghost"` | +| `fullWidth` | `size="wide"` | + +#### `ActionIconSplit` added + +- New compound component for the UI3 `Button icon split` set (41x24 small / 49x32 large): `ActionIconSplit` root plus `Button` (primary action) and `Chevron` (16px trigger half) subparts. Each half supports `render`, `aria-expanded` fills follow the dialog-toggle spec. + +#### `ActionIcon` / `Toggle` API rework + +- `ActionIcon`: `activeAppearance` removed (the on state now always follows the UI3 dialog-toggle spec); new `variant="secondary"` for the stroked `Button icon` variant. +- `Toggle`: `stroke` replaced by `variant` — `'highlighted'` (default, previous fill behavior), `'default'` (icon-swap only, no pressed fill), `'secondary'` (the former `stroke` look with the `bordertranslucent` token). + +#### Menu items: UI3 tokens + `ItemSwitch` + +- `DropdownMenu.ItemSwitch` and `ContextMenu.ItemSwitch` added: a `menuitemcheckbox` row with a Switch cell on the leading side (the spec's `Menu row/Toggle`). Clicking the row toggles the switch; the menu stays open. +- Menu row highlight now uses `bg-menu-selected` (was `bg-selected-strong` — visibly wrong in FigJam dark). `ItemBadge` uses `bg-toolbar` + `text-onbrand`; `ItemCheck` renders the spec's plain 16px check icon instead of a filled chip. Disabled items unify on `text-menu-disabled` / `icon-menu-disabled`. + +#### Fields: UI3 variants and states + +- `Select.Trigger`: new `stroke` prop (`Stroke=False` renders borderless on `bg-default`); focus and open states are a single `border-selected` swap (the extra outline ring is removed); the open value shows the spec's `texthighlight` block; the `icon` lead is a flush 24px cell, and on `size="large"` a 32px cell holding the spec's 24px `bg-secondary` well. +- `VariableChip` is 16px tall (was 18px) and uses `border-disabled` when disabled. +- New `VariableCell`: the spec's `Variable Cell` — a 24px bordered row with the variable icon cell plus a label. +- `ValueField.NumericSelect`: the dropdown segment uses `bg-selected` + `border-selected` while open (the spec's selected-chevron state). + +#### Slider and Swatch: UI3 spec variants + +- `Slider`: new `modified` prop renders the spec's `Stroke (Modified)` thumb (`bg-default` knob, `icon-onbrand` ring, elevation-300). Hue/opacity thumbs now embed the spec's 12px `_Chit 24` square instead of a 6px disc. Disabled thumb uses `bg-disabled` (was `bg-disabled-secondary`); the track and gradient-chit rings use the `bordertranslucent` token (were hardcoded); stepper ticks use `icon-tertiary` / `icon` (were `black/30` / `black/85`). +- `Swatch`: new `type="opacity"` (left half solid, right half at ~24% alpha — the spec's `Type=Opacity`) and `shape="circle"` (the spec's 16px round chit). The inset ring now uses `bordertranslucent` (same values as the previous hardcoded hexes — no visual change). + +#### Avatar and Badge: UI3 spec variants + +- `Avatar`: initials fills now bind to the `multiplayer*` / `textonmultiplayer*` tokens (were hardcoded light-theme hexes — dark themes were visibly wrong). Colors renamed to spec names (`magenta`→`pink`, `slate`→`grey`); new `overflow-unread` / `overflow-read` colors for "+N" avatars; new `disabled` prop (`bg-tertiary` + `text`); `shape="square"` uses `rounded-figma-sm` at `size="small"` (spec r=2). +- `Badge`: `size="small"` (14px) removed — no such badge in the spec; `size="large"` added (16px with 13px text — the spec's "Badge large"). New `variant="alt"` for the "Badge small alt" subtle-fill family (`default`/`count`/`count-inactive`/`strong`); new colors `figjam`, `menu`, `merged`, `archived`; the `warning` strong fill uses `icon-warning` per spec. New `BadgeDot` (the spec's "Badge Dot": 9px `bg-default` pill + 5px `bg-brand` dot). + +#### Radio, Tabs, Toast, Visual Bell, ScrollArea: UI3 tokens and states + +- `Radio` button variant: default border now uses `bordertranslucent` (was opaque `border`). +- `Tabs`: trigger gains the spec's `border-selected` focus ring; unselected hover uses `bg-hover` (same values, correct token). +- `Toast`: surface uses `bg-menu` / `text-menu` tokens (same values); shadow upgraded to the spec's "LEGACY - HUD shadow" via new `shadow-figma-hud` utility. +- `VisualBell`: `color="default"` is now the spec's light bell (`bg-default` + `text` + elevation-100) — replaces the dark `#2c2c2c` hardcoded fill. Action/dismiss edges use `bordertranslucent`; hover/active use the secondary-button transparent tokens. +- `ScrollArea`: thumb uses the `scrollbar` token via new `bg-figma-scrollbar` utility (was `text-secondary`). + +#### Remaining UI3 audit items + +- `Switch`: `indeterminate` renders the spec's `icon.16.mixed` glyph (16px, `icon-onbrand`; `icon-disabled` when disabled) instead of a hand-drawn bar. +- `Input`/`Textarea`: disabled state follows the spec per size — `size="default"` keeps `bg-default` + `border-default` stroke; `size="large"` stays `bg-secondary` with no stroke. +- Disabled colors aligned to the spec's tokens where values are identical: `text-tertiary` on `Select`, `ToggleGroup` items and labels, and all ValueField elements; `border-default` on ValueField outlines. +- `LinkTooltip.Action`: new `strong` prop for the spec's `body.medium.strong` CTA weight (w550). +- `Separator`: documented per-surface divider tokens — menu/tooltip surfaces should pass `bg-figma-border-menu`/`bg-figma-border-tooltip`. +- `AlertDialog` story CTA gap aligned to the Modal footer's 8px rhythm. + +#### Tokens: generated from `packages/theme/tokens/` + +- `@thinkbuff/figma-theme/colors` no longer exports `BaseThemeCSSVariables`/`BaseThemeCSSVariableKey` — each mode map is complete on its own. +- New exports: `FigJamDarkThemeCSSVariables`, `DevModeLightThemeCSSVariables`, `DevModeDarkThemeCSSVariables`, `SlidesLightThemeCSSVariables`, `SlidesDarkThemeCSSVariables` (+ types). +- Token values are now generated from `packages/theme/tokens/` via `pnpm tokens:import`; the FigJam block no longer claims `:root` (that selector is Figma light only). + +#### Primitives: Radix UI → Base UI (`@base-ui/react`) + +- `asChild` → `render` everywhere (e.g. `} />`). Use `nativeButton={false}` when rendering a non-button element through `Button`. +- Change callbacks receive a second `eventDetails` argument (`onValueChange`, `onCheckedChange`, `onPressedChange`, `onOpenChange`). Dismissal is cancelled via `eventDetails.cancel()` instead of `event.preventDefault()` on `onPointerDownOutside` / `onEscapeKeyDown` / `onInteractOutside` (removed). +- State attributes follow Base UI: `data-checked`, `data-unchecked`, `data-pressed`, `data-active`, `data-popup-open`, `data-highlighted`, `data-starting-style` / `data-ending-style`. Open triggers are styled through `aria-expanded`. +- Overlays (`Popover`, `Tooltip`, `DropdownMenu`, `Select`) position through a Base UI Positioner: `avoidCollisions` and `updatePositionStrategy` are removed, `collisionPadding` defaults to 5, and `sideOffset` measures to the popup edge (no automatic arrow height). +- `Checkbox`: `checked="indeterminate"` → `indeterminate` prop. +- `ToggleGroup`: `type="single" | "multiple"` → `multiple`; `value` / `defaultValue` are always arrays. +- `Slider`: `onValueCommit` → `onValueCommitted`; `minStepsBetweenThumbs` → `minStepsBetweenValues`; `inverted` removed; `SliderRoot` wraps a Base UI `Control`. +- `ScrollArea`: `type` / `scrollHideDelay` removed; the scrollbar shows while hovering or scrolling. +- `Tabs`: `activationMode` → `activateOnFocus` (default `false`: arrow keys move focus without selecting); `forceMount` → `keepMounted`; disabled tabs stay focusable. +- `Tooltip`: `delayDuration` → `delay`, `skipDelayDuration` → `timeout`, `disableHoverableContent` → `disableHoverablePopup`; `delay` / `closeDelay` live on the trigger. +- `DropdownMenu`: `Item onSelect` → `onClick`; `Label` must be inside `Group` / `RadioGroup`; checkbox and radio items keep the menu open. +- `Select`: pass `items` to `Root` so the trigger shows labels; `position` → `alignItemWithTrigger`; `Item asChild` removed; the empty value is `null`; `Label` must be inside `Group`; focus returns to the trigger on close. +- `Dialog` / `AlertDialog` / `Sheet`: `Overlay` renders Base UI `Backdrop`; `AlertDialog.Action` and `AlertDialog.Cancel` both close the dialog. +- `Label` is a native `