Skip to content

feat(themer): move @sanity/themer from sanity-io/ui, with a Storybook and ui's Changesets setup - #2046

Merged
stipsan merged 12 commits into
mainfrom
cody/move-themer-to-plugins-a30a
Sep 29, 2026
Merged

stipsan merged 12 commits into
mainfrom
cody/move-themer-to-plugins-a30a

Conversation

@stipsan

@stipsan stipsan commented Sep 28, 2026 •

Copy link
Copy Markdown
Member

Moves @sanity/themer from sanity-io/ui (packages/themer) to plugins/@sanity/themer, adds a React Storybook in dev/storybook modeled on sanity-io/ui's apps/storybook, and puts the whole repo on sanity-io/ui's Changesets setup.

What's in this PR

  1. Changesets setup identical to sanity-io/ui (repo-wide). .changeset/config.json gains "format": "oxfmt" and "bumpVersionsWithWorkspaceProtocolOnly": true (only the changelog repo differs from ui's). @changesets/cli ^3.0.3 and @changesets/changelog-github ^1.0.1 are root devDependencies, and there's a changeset script. release.yml drops the npm@11 pin, since @changesets/cli 3.0.3 handles npm 12's pnpm info --json output (pnpm.ts's parseInfoResult doesn't unwrap npm 12's array-wrapped changesets/changesets#2274). The Renovate changesets workflow is ui's.
  2. Catalog aligned with sanity-io/ui for what themer needs: @sanity/ui ^4.2.7, @sanity/icons ^5.2.2, motion ^13.4.4, @sanity/vanilla-extract-vite-plugin ^0.2.20. These are the ranges @sanity/themer@0.8.0 was published with. As AGENTS.md asks for uniform shared-dependency updates, one combined patch changeset covers the 43 published plugins whose dependencies ranges change with the @sanity/ui, @sanity/icons and motion bumps; @sanity/vanilla-extract-vite-plugin is a devDependency everywhere, so it doesn't reach a published range.
  3. The move. Source copied from sanity-io/ui main (b28d66c), which is exactly the published 0.8.0. The version and CHANGELOG carry over, so the next release is 0.8.1. package.json follows the monorepo template (plugins repository links, build/prepack scripts) with the published exports, sideEffects, and dependency ranges. @sanity/themer-legacy (moving to sanity-io/themer) is the published ^0.2.1. xstate, @xstate/react, and scroll-into-view-if-needed move to the catalog, since other plugins now share them; sanity-plugin-dashboard-widget-vercel gets its own patch changeset for its xstate range (^5.32.5 → ^5.33.2). The test studio switches from ^0.8.0 on npm to workspace:*; themerTool() was already wired in its Home workspace.
  4. Storybook in dev/storybook, next to the test studios, set up like sanity-io/ui's apps/storybook: Storybook 10.6 with the a11y, docs, links, themes and vitest addons, the React Compiler through @vitejs/plugin-react, and the vanilla-extract plugin for workspace plugins' .css.ts source. The preview imports @sanity/ui/styles.css, since the published dist doesn't. The global decorator renders every story in the theme buildTheme from @sanity/themer builds from a preset picked in the toolbar, next to the light/dark toggle. It adds pnpm dev:storybook, pnpm storybook:build, pnpm test:browser, ui's apps/storybook/vercel.json unchanged (as dev/storybook/vercel.json), a storybook-test CI job like ui's, and eslint-plugin-storybook's CSF rules through oxlint's JS plugins, as in ui. Unlike ui's job, storybook-test also installs Playwright's system deps when it restores the browser cache, as this repo's e2e workflow does (Bugbot flagged that the cache doesn't hold them). Like the other dev/* apps, it's skipped by pretest and the CI build job.

Layout

No existing folder is renamed or moved, and there's no new top-level folder. @sanity/themer goes into the existing plugins/@sanity/ folder next to the other @sanity/* plugins, not ui's packages/themer. The Storybook goes into the existing dev/ folder as dev/storybook, next to dev/test-studio and dev/e2e-studio, so the dev/* workspace glob and the ./dev/* build filters already cover it. The root only gains workspace-level config.

How faithful the move is

  • Build output vs. the npm tarball of 0.8.0: JavaScript is identical apart from chunk hashes, and bundle.css is byte-identical. The .d.ts files have the same API; tsdown 0.22 writes declare …; export {…} where 0.23 wrote export declare ….
  • Source changes: type modifiers on type-only imports (plugins' strictest tsconfig has verbatimModuleSyntax), one inline oxlint-disable-next-line react/exhaustive-effect-dependencies with its reason (the rule is on here and off in ui; the [palette] dependency is deliberate), and the README license link. The tsconfig is strictest without noUncheckedIndexedAccess, which ui never enabled (sanity-plugin-graph-view and plugin-kit do the same).
  • No React Compiler typings stub: ui's typings/babel-plugin-react-compiler.d.ts isn't carried over. Here the real babel-plugin-react-compiler types resolve (it's a linked optional peer of @sanity/tsdown-config), so defineConfig() picks the single-config overload, and a wrong reactCompiler value is still flagged (checked with a bad target, a misspelled transform, a number target and an unknown key).
  • Tests: themer's Vitest config sets testTimeout: 30_000. PR CI here runs every project with coverage, and the whole-theme buildTheme tests took 8s there against the 5s default (ui runs them without coverage).
  • Versions: every themer runtime dependency and the whole Storybook stack resolve to the same versions as in ui's lockfile.

What Cody needs to do

  • npm trusted publishing: publishing runs from .github/workflows/release.yml, which this repo already had; it now uses ui's version/publish flow. The trusted publisher for @sanity/themer must be repository sanity-io/plugins, workflow release.yml, no environment.
  • Vercel: the Storybook carries ui's apps/storybook/vercel.json unchanged, as dev/storybook/vercel.json (pnpm build into storybook-static). To deploy it like ui's sanity-ui-storybook, create a project with root directory dev/storybook; the file supplies the build command and output directory. No other Vercel config is touched: dev/e2e-studio/vercel.json and the plugins-studio and plugins-e2e-test-studio projects stay as they are, and both previews built fine on this PR.
  • Secrets and env vars: none new. The touched workflows use names this repo already has: ECOSPARK_APP_ID, ECOSPARK_APP_PRIVATE_KEY, vars.ECOSPARK_CLIENT_ID, TURBO_TOKEN and vars.TURBO_TEAM. storybook-test uses none.
  • Ruleset: no swap needed. The main ruleset requires lint, build, test and the two Socket Security checks, and those job names are unchanged. Optionally, to match ui (which requires build, knip, Test node@lts / ubuntu-latest, storybook-test and its two Vercel checks), add storybook-test and knip, and the Storybook's Vercel check once that project exists.
  • CODEOWNERS: untouched, per the transfer convention. Add an owner for plugins/@sanity/themer if you want one.

Open questions

  1. Patch, not major. The transfer skill defaults to a major, but themer is 0.x, nothing breaks, the next release should continue from the published version, and themer's own deprecation notice reserves 1.0 for removing @sanity/themer/legacy. So the changeset is a patch (0.8.1). The color move made the same call.
  2. Stories. sanity-io/ui had no themer stories. I ported the ui stories that exercise what themer builds (theme Debug/Build/Canvas/Color, and the color palette story on buildPalette's output) and added stories for the tool's theme thumbnails and navbar color wheel. Those two aren't public API, so their stories import them from plugins/@sanity/themer/src by relative path. Keep them, or public API only?
  3. sanity peer range. I kept 0.8.0's optional ^3 || ^4 || ^5 || ^6 instead of the monorepo's catalog:peer (^5 || ^6.0.0-0), which would drop Studio v3/v4. As with 0.8.0 from npm, it doesn't match the sanity@next prereleases here, so the test studio shows a peer warning.
  4. bumpVersionsWithWorkspaceProtocolOnly. With ui's setting, Changesets no longer rewrites @sanity/sfcc's intentionally wide sanity-plugin-internationalized-array peer (^4.0.0 || ^5.0.0), the only internal dependency that doesn't use workspace:. That seems right, but it's a behavior change.

Remaining alignment with sanity-io/ui (not in this PR)

This PR stays scoped to the themer move. Matching ui's tooling happens in five separate PRs, one per tool, each owned by another worker. All five exist:

All five also touch files this PR changes (pnpm-workspace.yaml, pnpm-lock.yaml and AGENTS.md; package.json and README.md for #2047 and #2048; oxlint.config.ts for #2049; .github/workflows/ci.yml and .gitignore for #2050), so whichever PR merges later rebases and regenerates the lockfile.

Smaller differences left to Renovate or later PRs: oxc-transform-react ^0.147 → ^0.151 (#1988), @types/node ^24.13 → ^24.19, jsdom ^29 → ^30, knip ^6.33 → ^6.38, ui's macOS test run, and ui's root engines.node: ">=22.13".

Verification

  • pnpm format, pnpm knip, pnpm lint, pnpm build (54 tasks, including both studios and the Storybook), pnpm test run (235 files, 1,509 tests) and pnpm test:browser (7 stories) all pass locally, starting from pnpm install --frozen-lockfile.
  • A changeset version dry run in a scratch worktree, with ui's config and GITHUB_TOKEN, bumps themer 0.8.0 → 0.8.1 and dashboard-widget-vercel 4.0.28 → 4.0.29 (before the combined changeset; changeset status now plans 46 patch releases: the 43 plugins, themer, and sanity-plugin-studio-smartling and sanity-plugin-transifex, which follow sanity-translations-tab as internal dependents), credits @stipsan through the author: line, and writes CHANGELOGs that pass oxfmt --check.
  • zizmor .github --min-severity high reports no findings.

Transfer verification

  • package.json dependencies, peerDependencies and exports verified against the published 0.8.0
  • LICENSE not added: ui's credits only Sanity.io; the README links this repo's LICENSE
  • Test studio uses the workspace package (themerTool() was already wired)
  • pnpm format, pnpm knip, pnpm lint, pnpm build, pnpm test run pass
  • Changeset added (patch; see open question 1)
  • Trusted publishing for @sanity/themer configured with sanity-io/plugins and release.yml

Maintainer follow-up

  • Remove packages/themer from sanity-io/ui (with its entries in ui's root build/test scripts and pkg-pr-new.yml) once this lands, on your go-ahead
  • Point ui's docs and AGENTS.md mentions of @sanity/themer to plugins/@sanity/themer
  • Transfer open themer issues from sanity-io/ui and label them themer

Screenshots

The theme cards of every preset, in the dark scheme:

Storybook: themer tool theme thumbnails for every preset

The palette buildPalette generates for the Verdant preset, light scheme (its backgrounds replace black and white, its accent the blue scale, its text color the gray scale):

Storybook: generated Verdant palette with contrast badges

The Debug theme story in the Pink Synth preset, both schemes:

Storybook: Debug theme story in the Pink Synth preset

To show artifacts inline, enable in settings.

Open in Web Open in Cursor 

Match sanity-io/ui's Changesets config, versions, scripts and release flow:

- `.changeset/config.json` adds `"format": "oxfmt"` and
  `"bumpVersionsWithWorkspaceProtocolOnly": true`; only the changelog
  `repo` differs from ui's
- `@changesets/cli` ^3.0.3 and `@changesets/changelog-github` ^1.0.1 as
  root devDependencies, plus a `changeset` script
- `release.yml` drops the npm@11 pin: @changesets/cli 3.0.3 unwraps the
  `pnpm info --json` output of npm 12 (changesets/changesets#2274)
- the Renovate changesets workflow takes ui's name and also runs for
  `squiggler-legacy[bot]`
- the `@sanity/plugin-kit/oxfmt` preset drops the Prettier-era
  `singleQuote: false` override for `.changeset/*.md`, now that Changesets
  formats what it writes with oxfmt itself
…a-extract-vite-plugin with sanity-io/ui

Bump the catalog to the versions sanity-io/ui develops @sanity/themer
against, which are also the ranges @sanity/themer@0.8.0 was published
with: @sanity/ui ^4.2.7, @sanity/icons ^5.2.2, motion ^13.4.4 and
@sanity/vanilla-extract-vite-plugin ^0.2.20. The lockfile now resolves a
single @sanity/ui (4.2.7, the version sanity@next uses too), @sanity/icons
and motion instead of two of each.
…y/themer

Copy `packages/themer` from the latest sanity-io/ui main (the source of
@sanity/themer@0.8.0), keeping its version and changelog so the next
release continues at 0.8.1:

- package.json follows the monorepo template (plugins repository links,
  `build`/`prepack` scripts) with the published exports, `sideEffects`
  and dependency ranges; @sanity/themer-legacy, which moves to
  sanity-io/themer, is the published ^0.2.1, and React stays ^19.3 as the
  tool needs `ViewTransition`
- tsdown/vitest configs from the templates, with themer's own build
  options (explicit entries, React Compiler target 19, unminified
  bundle.css without the light-dark() polyfill) and a package exports test
- tsconfig extends @sanity/tsconfig/strictest without
  `noUncheckedIndexedAccess`, which sanity-io/ui never enabled (like
  sanity-plugin-graph-view and plugin-kit); type-only imports are marked
  for `verbatimModuleSyntax`
- the build output matches the published 0.8.0: identical JavaScript
  (modulo chunk hashes) and bundle.css
- xstate, @xstate/react and scroll-into-view-if-needed move to the
  catalog, now that other plugins share them
- the test studio uses the workspace package instead of ^0.8.0 from npm
…themer

Set up `apps/storybook` like sanity-io/ui's (Storybook 10.6 with the a11y,
docs, links, themes and vitest addons, the React Compiler through
@vitejs/plugin-react, and the vanilla-extract plugin for workspace
plugins' `.css.ts` source). The global decorator renders every story in
the theme that @sanity/themer's `buildTheme` builds from the preset picked
in a toolbar menu, next to the light/dark toggle, and the preview imports
`@sanity/ui/styles.css` since the published @sanity/ui doesn't import it.

@sanity/themer had no stories in sanity-io/ui, so the stories ported are
the ones that exercise what it builds: ui's theme stories (Debug, Build,
Canvas, Color) render the themer theme, ui's color palette story renders
the palettes `buildPalette` generates, and the tool's theme thumbnails and
navbar color wheel get stories of their own.

- `pnpm dev:storybook`, `pnpm storybook:build` and `pnpm test:browser`
  root scripts; `pretest` and the CI build skip `./apps/*` like `./dev/*`
- a `storybook-test` CI job that renders every story in headless Chromium,
  like sanity-io/ui's
- eslint-plugin-storybook's recommended CSF rules through oxlint's JS
  plugins, as in sanity-io/ui
@changeset-bot

changeset-bot Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 5333eca

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 46 packages
Name Type
sanity-plugin-dashboard-widget-vercel Patch
@sanity/assist Patch
@sanity/block-insert-picker Patch
@sanity/code-input Patch
@sanity/color-input Patch
@sanity/cross-dataset-duplicator Patch
@sanity/dashboard Patch
@sanity/debug-live-sync-tags Patch
@sanity/debug-preview-url-secret-plugin Patch
@sanity/document-internationalization Patch
@sanity/embeddings-index-ui Patch
@sanity/form-toolkit Patch
@sanity/google-maps-input Patch
@sanity/hierarchical-document-list Patch
@sanity/language-filter Patch
@sanity/orderable-document-list Patch
@sanity/personalization-plugin Patch
@sanity/rich-date-input Patch
@sanity/sanity-plugin-async-list Patch
@sanity/sfcc Patch
@sanity/studio-secrets Patch
@sanity/table Patch
@sanity/vercel-protection-bypass Patch
sanity-plugin-aprimo Patch
sanity-plugin-asset-source-unsplash Patch
sanity-plugin-bynder-input Patch
sanity-plugin-cloudinary Patch
sanity-plugin-dashboard-widget-document-list Patch
sanity-plugin-dashboard-widget-netlify Patch
sanity-plugin-documents-pane Patch
sanity-plugin-google-translate Patch
sanity-plugin-graph-view Patch
sanity-plugin-hotspot-array Patch
sanity-plugin-iframe-pane Patch
sanity-plugin-internationalized-array Patch
sanity-plugin-markdown Patch
sanity-plugin-media Patch
sanity-plugin-mux-input Patch
sanity-plugin-shopify-assets Patch
sanity-plugin-utils Patch
sanity-plugin-workflow Patch
sanity-plugin-workspace-home Patch
sanity-translations-tab Patch
@sanity/themer Patch
sanity-plugin-studio-smartling Patch
sanity-plugin-transifex Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
plugins-studio Ready Ready Preview Sep 29, 2026 1:48am UTC
1 Skipped Deployment
Project Deployment Actions Updated
plugins-e2e-test-studio Ignored Ignored Sep 29, 2026 1:48am UTC

Request Review

@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

✅ E2E Tests

🟢 43 passed • 🟡 1 flaky • view full report • view run

Studio: https://plugins-e2e-test-studio-blyayu331.sanity.dev

Datasets: pr-2046-chromium-36509530000, pr-2046-firefox-36509530000

@cursor cursor Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Playwright deps skipped on cache hit
    • Added a cache-hit branch running playwright install-deps chromium so the OS-level Playwright dependencies (which live on the runner, not in the cached ~/.cache/ms-playwright) are installed on every run.

Create PR

Or push these changes by commenting:

@cursor push e4a7606d9b
Preview (e4a7606d9b)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -63,6 +63,9 @@
       - name: Install Playwright browsers
         if: steps.cache-playwright-browsers.outputs.cache-hit != 'true'
         run: pnpm --filter plugins-storybook exec playwright install --with-deps chromium
+      - name: Install Playwright system dependencies
+        if: steps.cache-playwright-browsers.outputs.cache-hit == 'true'
+        run: pnpm --filter plugins-storybook exec playwright install-deps chromium
       - run: pnpm test:browser
       - uses: actions/upload-artifact@v7
         if: failure()

You can send follow-ups to the cloud agent here.

Reviewed by Cursor Bugbot for commit 780becb. Configure here.

Comment thread .github/workflows/ci.yml
The buildTheme tests build full themes and compare every color in them,
which took 8s of the 5s default on a CI shard with coverage on (3.5s
locally).
… browser cache

The browser cache only holds the browsers, not the system libraries that
`install --with-deps` puts on the runner, so a cache hit installs them
with `install-deps`, like the e2e workflow does.
The Storybook's Vercel config lives with the app, unchanged from
sanity-io/ui: `pnpm build` into `storybook-static`.
@github-actions

github-actions Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 35.3% 6421 / 18187
🔵 Statements 35.22% 6842 / 19426
🔵 Functions 31.45% 1716 / 5455
🔵 Branches 26.69% 3419 / 12810
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
plugins/@sanity/themer/src/index.ts 0% 0% 0% 0%
plugins/@sanity/themer/src/legacy/index.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/lib/mix.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/theme/buildPalette.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/theme/buildTheme.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/theme/hsl.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/theme/options.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/theme/presets.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/tool/AnimatedColorWheelIcon.tsx 54.54% 0% 37.5% 55% 52-88
plugins/@sanity/themer/src/tool/ThemerLayout.css.ts 0% 0% 0% 0%
plugins/@sanity/themer/src/tool/ThemerLayout.tsx 2.89% 0% 0% 3.22% 40-367
plugins/@sanity/themer/src/tool/ThemerNavbar.tsx 2.22% 0% 0% 2.77% 20-29, 49-165
plugins/@sanity/themer/src/tool/colorWheel.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/tool/context.ts 20% 0% 0% 20% 55-61
plugins/@sanity/themer/src/tool/imagePalette.ts 98.64% 94.62% 100% 99.21% 327, 487
plugins/@sanity/themer/src/tool/index.ts 0% 0% 0% 0%
plugins/@sanity/themer/src/tool/machine.ts 98.76% 97.91% 98.3% 98.61% 447
plugins/@sanity/themer/src/tool/options.ts 100% 100% 100% 100%
plugins/@sanity/themer/src/tool/plugin.tsx 75% 100% 50% 75% 67
plugins/@sanity/themer/src/tool/share.ts 94.11% 91.42% 100% 98.18% 49, 53, 101, 124
plugins/@sanity/themer/src/tool/snippet.ts 100% 92.85% 100% 100%
plugins/@sanity/themer/src/tool/storage.ts 98.44% 94.78% 100% 100% 160, 233
plugins/@sanity/themer/src/tool/themes.ts 96.77% 93.75% 100% 96% 153
plugins/@sanity/themer/src/tool/usePrefersReducedMotion.ts 12.5% 100% 0% 14.28% 6-28
plugins/@sanity/themer/src/tool/useStudioNavbarHeight.ts 3.57% 0% 0% 4.16% 21-64
plugins/@sanity/themer/src/tool/useTypedDeferredValue.ts 100% 100% 100% 100%
Generated in workflow #10199 for commit 5333eca by the Vitest Coverage Report Action

cursor Bot pushed a commit to sanity-io/themer that referenced this pull request Sep 28, 2026
The Playwright cache only restores the browsers, so on a hit the job never
installed the system packages `install --with-deps` adds on a miss. Same fix
as sanity-io/plugins#2046.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursor Bot pushed a commit to sanity-io/logos that referenced this pull request Sep 28, 2026
The Playwright browser cache only holds the browsers, not the system
packages that `install --with-deps` puts on the runner, so install them
separately on a cache hit (same fix as sanity-io/plugins#2046).

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursor Bot pushed a commit to sanity-io/icons that referenced this pull request Sep 28, 2026
The storybook-test job (copied from sanity-io/ui) only ran
`playwright install --with-deps` on a cache miss. The cache holds the
browsers but not the system packages --with-deps installs on the runner, so
on a cache hit Chromium could start without them. Run
`playwright install-deps chromium` on cache hits, the same fix as
sanity-io/plugins#2046.
stipsan added a commit to sanity-io/logos that referenced this pull request Sep 28, 2026
…g, CI, release and Storybook (#193)

* refactor: move @sanity/logos from sanity-io/ui into packages/logos

Copies packages/logos from the latest sanity-io/ui main (2.2.5, the latest
published version) including its changelog, tsdown build config and
Changesets-era package metadata. The repository and bugs URLs point back at
sanity-io/logos, and @sanity/color becomes a regular npm dependency
(^3.0.8, its latest release) since it's released from sanity-io/color.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: adopt sanity-io/ui's workspace tooling

Turns the repo into a pnpm workspace like sanity-io/ui: pnpm 12.6.0 with
the same catalog versions, oxlint (type-aware, incl. type checking) and oxfmt
configs, knip, TypeScript 7 settings, and the identical Changesets setup
(config, @changesets/* versions and changeset/release scripts). Drops
semantic-release, commitizen, commitlint/husky, prettier and pkg-utils.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* ci: replace semantic-release with sanity-io/ui's CI and Changesets release

Splits the old CI & Release workflow (main.yml) into ui's workflows:
ci.yml (lint + build, knip, Storybook browser tests), and release.yml,
which is identical to ui's and publishes with changesets/action through
npm trusted publishing (OIDC, no npm token). Also adds ui's shared
format-if-needed and changesets-from-conventional-commits workflows, the
pkg.pr.new preview publishing workflow, the setup action, and ui's
Renovate package rules.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* feat: replace the workshop with a Storybook in apps/storybook

Sets up a React Storybook modeled on sanity-io/ui's apps/storybook (same
addons, theme decorator, React Compiler via oxc and Vitest browser tests)
and ports ui's logo stories. The decorator uses the published @sanity/ui,
so its stylesheet is imported in the preview. Removes @sanity/ui-workshop.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* docs: document the workspace, Storybook and Changesets release flow

Replaces the "moved into the ui monorepo" README and adds CONTRIBUTING.md
and AGENTS.md modeled on sanity-io/ui's.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: vendor the agent skills sanity-io/ui uses for Storybook and tsdown

Copies ui's vendored stories, storybook-*, sanity-tsdown-config, deslop and
ui-demo skills (and their skills-lock.json entries); ui's Next.js-specific
skills are left out.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: add a changeset for releasing @sanity/logos from sanity-io/logos

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: move the workshop's vercel.json under apps/storybook

The repo root now only holds workspace-level config. The Vercel config
moves unchanged with the app it deploys, which is now the Storybook in
apps/storybook.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: use ui's Storybook vercel.json instead of the workshop's

The workshop's vercel.json belongs to the removed workshop, so it's
deleted rather than carried over. apps/storybook/vercel.json is ui's
apps/storybook/vercel.json unchanged, which sets the Storybook build
command and output directory.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* ci: install Playwright's system deps when the browser cache hits

The Playwright browser cache only holds the browsers, not the system
packages that `install --with-deps` puts on the runner, so install them
separately on a cache hit (same fix as sanity-io/plugins#2046).

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
…book

The Storybook is a dev app like the test studios, so it lives next to them
in `dev/` instead of a new top-level `apps/` folder, with its `vercel.json`
moving along unchanged. The `dev/*` workspace glob and the `./dev/*` build
filters of `pretest`, CI and pkg-pr-new already cover it, so their
`./apps/*` additions go; the storybook-test artifact path, the
no-uninstalled-addons manifest location and the docs point at the new path.
…he oxfmt alignment PR

Dropping the `.changeset/*.md` `singleQuote: false` override from the
`@sanity/plugin-kit/oxfmt` preset is formatter config, which the oxfmt
alignment (#2047) owns, not the Changesets setup (config, versions,
scripts and release flow). Restore the preset as it is on main, drop its
`@sanity/plugin-kit` patch changeset, and format the remaining changesets
with the preset's double quotes again.
Brings in the lock file maintenance from #2039. The only conflict was the
generated pnpm-lock.yaml: resolved by taking main's lockfile and letting
`pnpm install` re-apply this branch's manifest changes, which adds just the
plugins/@sanity/themer and dev/storybook importers and the catalog bumps
on top of main's refreshed resolutions.
@stipsan
stipsan marked this pull request as ready for review September 29, 2026 01:33
Copilot AI balanced review requested due to automatic review settings September 29, 2026 01:33
@stipsan
stipsan requested review from a team as code owners September 29, 2026 01:33
@stipsan
stipsan requested review from pedrobonamin and removed request for a team September 29, 2026 01:33

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Mandatory transfer and shared-dependency changeset requirements are not currently satisfied.

Review effort: Balanced
Findings: 2 Medium severity

Open (2)
What changed in this PR

Transfers @sanity/themer into the plugins monorepo, integrates it with the test Studio, and adds Storybook-based visual development and browser testing.

Changes:

  • Ports the themer package, presets, tooling UI, and tests.
  • Adds Storybook stories and a browser-test CI job.
  • Aligns Changesets, release automation, and shared dependency catalogs.

Release metadata needs correction: the transfer requires a major changeset, and shared catalog bumps require changesets for affected packages.

File Description
.changeset/​config.json Aligns Changesets behavior with the UI repository.
.changeset/​themer-plugins-monorepo.md Adds the themer release entry.
.changeset/​update-xstate.md Records the dashboard XState update.
.github/​renovate.json Updates Renovate’s changeset workflow.
.github/​workflows/​ci.yml Adds Storybook browser testing.
.github/​workflows/​release.yml Updates Changesets-based publishing.
.gitignore Ignores Storybook build output.
AGENTS.md Documents Storybook and release workflows.
README.md Lists themer and Storybook commands.
dev/​storybook/​.storybook/​main.ts Configures Storybook and Vite plugins.
dev/​storybook/​.storybook/​preview.tsx Adds global theme controls and decorators.
dev/​storybook/​README.md Documents Storybook usage.
dev/​storybook/​package.json Defines the Storybook workspace.
dev/​storybook/​stories/​themer/​AnimatedColorWheel.stories.tsx Adds color-wheel stories.
dev/​storybook/​stories/​themer/​ImagePaletteSection.stories.tsx Adds image-palette stories.
dev/​storybook/​stories/​themer/​ThemeList.stories.tsx Adds theme-list stories.
dev/​storybook/​stories/​themer/​build/​BuildThemeStory.tsx Builds theme story fixtures.
dev/​storybook/​stories/​themer/​build/​ColorThemeStory.tsx Builds color-theme fixtures.
dev/​storybook/​stories/​themer/​build/​ThemeColorCard.tsx Renders theme color cards.
dev/​storybook/​stories/​themer/​build/​ThemeDebugCard.tsx Renders theme debugging cards.
dev/​storybook/​stories/​themer/​build/​helpers.ts Provides story theme helpers.
dev/​storybook/​stories/​themer/​build/​types.ts Defines story helper types.
dev/​storybook/​stories/​themer/​theme/​BuildThemeStoryView.tsx Adds the build-theme view.
dev/​storybook/​stories/​themer/​theme/​CanvasThemeStoryView.tsx Adds the canvas-theme view.
dev/​storybook/​stories/​themer/​theme/​ColorThemeStoryView.tsx Adds the color-theme view.
dev/​storybook/​stories/​themer/​theme/​DebugThemeStoryView.tsx Adds the debug-theme view.
dev/​storybook/​stories/​themer/​theme/​ThemeColorItem.tsx Renders story color values.
dev/​storybook/​stories/​themer/​theme/​Theme.stories.tsx Defines theme stories.
dev/​storybook/​stories/​themer/​utils/​usePrefersReducedMotion.ts Adds reduced-motion detection.
dev/​storybook/​stories/​themer/​utils/​useStringifiedValue.ts Formats story values.
dev/​storybook/​vercel.json Configures Storybook deployment.
dev/​storybook/​vite-env.d.ts Adds Vite environment types.
dev/​storybook/​vitest.config.ts Configures browser story tests.
dev/​test-studio/​package.json Uses the workspace themer package.
dev/​test-studio/​sanity.config.ts Integrates themer into the Studio.
package.json Adds workspace scripts and tooling.
plugins/​@sanity/​themer/​package.json Defines the transferred package.
plugins/​@sanity/​themer/​CHANGELOG.md Preserves package release history.
plugins/​@sanity/​themer/​README.md Documents the themer package.
plugins/​@sanity/​themer/​src/​index.test.ts Verifies package exports.
plugins/​@sanity/​themer/​src/​index.ts Defines public exports.
plugins/​@sanity/​themer/​src/​plugin.ts Defines the Studio plugin.
plugins/​@sanity/​themer/​src/​presets/​index.ts Exports theme presets.
plugins/​@sanity/​themer/​src/​presets/​lunaria.ts Defines the Lunaria preset.
plugins/​@sanity/​themer/​src/​presets/​midnight.ts Defines the Midnight preset.
plugins/​@sanity/​themer/​src/​presets/​pinkSynth.ts Defines the Pink Synth preset.
plugins/​@sanity/​themer/​src/​presets/​rust.ts Defines the Rust preset.
plugins/​@sanity/​themer/​src/​presets/​verdant.ts Defines the Verdant preset.
plugins/​@sanity/​themer/​src/​theme/​buildPalette.test.ts Tests palette generation.
plugins/​@sanity/​themer/​src/​theme/​buildPalette.ts Generates accessible palettes.
plugins/​@sanity/​themer/​src/​theme/​buildTheme.test.ts Tests theme generation.
plugins/​@sanity/​themer/​src/​theme/​buildTheme.ts Builds Sanity UI themes.
plugins/​@sanity/​themer/​src/​theme/​index.ts Exports theme utilities.
plugins/​@sanity/​themer/​src/​theme/​types.ts Defines theme types.
plugins/​@sanity/​themer/​src/​tool/​AnimatedColorWheel.tsx Renders the animated color wheel.
plugins/​@sanity/​themer/​src/​tool/​constants.ts Defines tool constants.
plugins/​@sanity/​themer/​src/​tool/​custom/​CustomTheme.tsx Renders custom-theme controls.
plugins/​@sanity/​themer/​src/​tool/​custom/​CustomThemeOptions.tsx Edits custom-theme options.
plugins/​@sanity/​themer/​src/​tool/​custom/​helpers.ts Provides custom-theme helpers.
plugins/​@sanity/​themer/​src/​tool/​image/​imageColor.test.ts Tests image color extraction.
plugins/​@sanity/​themer/​src/​tool/​image/​imageColor.ts Extracts representative image colors.
plugins/​@sanity/​themer/​src/​tool/​image/​rgba.ts Implements RGBA utilities.
plugins/​@sanity/​themer/​src/​tool/​ImagePaletteSection.tsx Handles image-generated palettes.
plugins/​@sanity/​themer/​src/​tool/​index.ts Exports tool components.
plugins/​@sanity/​themer/​src/​tool/​machine.test.ts Tests tool state transitions.
plugins/​@sanity/​themer/​src/​tool/​machine.ts Implements the tool state machine.
plugins/​@sanity/​themer/​src/​tool/​NavigationBar.tsx Renders tool navigation.
plugins/​@sanity/​themer/​src/​tool/​palette/​PalettePreview.tsx Previews generated palettes.
plugins/​@sanity/​themer/​src/​tool/​palette/​PalettePreviewOptions.tsx Configures palette previews.
plugins/​@sanity/​themer/​src/​tool/​presets/​ThemePresetDetails.tsx Displays preset details.
plugins/​@sanity/​themer/​src/​tool/​presets/​ThemePresetDetailsOptions.tsx Configures preset details.
plugins/​@sanity/​themer/​src/​tool/​presets/​ThemePresetThumbnail.tsx Renders preset thumbnails.
plugins/​@sanity/​themer/​src/​tool/​presets/​ThemePresetThumbnails.tsx Renders the preset collection.
plugins/​@sanity/​themer/​src/​tool/​providers/​ThemeProvider.tsx Provides active theme state.
plugins/​@sanity/​themer/​src/​tool/​providers/​WorkspaceProvider.tsx Provides workspace context.
plugins/​@sanity/​themer/​src/​tool/​Sidebar.tsx Renders the theme editor sidebar.
plugins/​@sanity/​themer/​src/​tool/​storage.test.ts Tests persisted-state migration.
plugins/​@sanity/​themer/​src/​tool/​storage.ts Persists and sanitizes tool state.
plugins/​@sanity/​themer/​src/​tool/​StyledLogo.tsx Renders the themed logo.
plugins/​@sanity/​themer/​src/​tool/​ThemeList.tsx Displays saved themes.
plugins/​@sanity/​themer/​src/​tool/​ThemeListMenu.tsx Provides theme-list actions.
plugins/​@sanity/​themer/​src/​tool/​ThemePreview.tsx Renders the Studio preview.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemeColorSchemeMenu.tsx Provides scheme selection.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemeColorSchemeMenuOptions.tsx Configures scheme options.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemeColorSchemeSelector.tsx Selects the active scheme.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemeColorTokenMenu.tsx Provides color-token selection.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemeColorTokenMenuOptions.tsx Configures token options.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemePreview.tsx Renders theme details.
plugins/​@sanity/​themer/​src/​tool/​theme/​ThemePreviewOptions.tsx Configures theme previews.
plugins/​@sanity/​themer/​src/​tool/​ThemerLayout.tsx Composes the complete tool layout.
plugins/​@sanity/​themer/​src/​tool/​types.ts Defines tool types.
plugins/​@sanity/​themer/​src/​tool/​ui/​EnhancedTooltip.tsx Adds enhanced tooltips.
plugins/​@sanity/​themer/​src/​tool/​ui/​FieldHeader.tsx Renders field headings.
plugins/​@sanity/​themer/​src/​tool/​ui/​ScrollArea.tsx Provides scrollable regions.
plugins/​@sanity/​themer/​src/​tool/​ui/​Stat.tsx Renders theme statistics.
plugins/​@sanity/​themer/​src/​tool/​ui/​ViewTransition.tsx Wraps view transitions.
plugins/​@sanity/​themer/​src/​tool/​useActiveTheme.ts Resolves the active theme.
plugins/​@sanity/​themer/​src/​tool/​useStudioNavbarHeight.ts Tracks Studio navbar height.
plugins/​@sanity/​themer/​src/​tool/​useThemeIFrameSrc.ts Resolves preview iframe URLs.
plugins/​@sanity/​themer/​src/​tool/​useThemeShare.ts Implements theme sharing.
plugins/​@sanity/​themer/​src/​tool/​useThemeStyles.ts Applies generated theme styles.
plugins/​@sanity/​themer/​src/​tool/​useTypedState.ts Adds typed machine state access.
plugins/​@sanity/​themer/​src/​tool/​useTypedStateFrom.ts Adds typed actor state access.
plugins/​@sanity/​themer/​tsconfig.json Configures TypeScript compilation.
plugins/​@sanity/​themer/​tsdown.config.ts Configures package builds.
plugins/​@sanity/​themer/​vitest.config.ts Configures package tests.
plugins/​sanity-plugin-dashboard-widget-vercel/​package.json Moves XState to the catalog.
plugins/​sanity-plugin-mux-input/​package.json Catalogs scrolling dependencies.
pnpm-lock.yaml Resolves added and updated dependencies.
pnpm-workspace.yaml Updates shared dependency catalogs.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@@ -0,0 +1,7 @@
---
"@sanity/themer": patch
Comment thread pnpm-workspace.yaml
'@sanity/tsdown-config': ^0.27.1
'@sanity/types': ^6.16.0
'@sanity/ui': ^4.0.7
'@sanity/ui': ^4.2.7
package.json and pnpm-workspace.yaml keep both sides: main's pnpm 12.6.0
pin, vercel bump, autoDedupe and exclude lists, and this branch's
Changesets devDependencies, Storybook scripts and catalog entries.
pnpm-lock.yaml is main's, regenerated with pnpm 12.6.0 invoked directly
and deduped once more, so debug-preview-url-secret-plugin keeps main's
optional-peer variant and the diff against main holds only this branch's
changes.
…@sanity/icons and motion catalog bumps

The catalog now resolves @sanity/ui ^4.2.7, @sanity/icons ^5.2.2 and
motion ^13.4.4, which raises the ranges 43 published plugins declare in
their dependencies. AGENTS.md asks for one combined changeset for a
uniform shared-dependency update like this. @sanity/themer is left out:
its ranges match what 0.8.0 published. @sanity/vanilla-extract-vite-plugin
is a devDependency everywhere, so its bump doesn't reach a published range.
Copilot AI review requested due to automatic review settings September 29, 2026 01:47
@stipsan
stipsan merged commit 546e3a9 into main Sep 29, 2026
25 checks passed
@stipsan
stipsan deleted the cody/move-themer-to-plugins-a30a branch September 29, 2026 01:50
@squiggler-app squiggler-app Bot mentioned this pull request Sep 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The sidebar splitter has unresolved keyboard and ARIA accessibility defects, and the transfer’s required major changeset remains unresolved.

Review effort: Balanced
Findings: 3 Medium severity

Open (3)

Comment on lines +86 to +87
Home: MAXIMUM_WIDTH,
End: -MAXIMUM_WIDTH,
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Brings in the themer move (#2046). Git merged every file without
conflicts, including this branch's oxlint.config.ts rules next to
#2046's Storybook lint wiring.

pnpm-lock.yaml: took main's lockfile and let pnpm 12.6.0 re-apply this
branch's catalog change, so it differs from main only by the oxlint and
oxlint-tsgolint catalog specifiers. The re-resolution flipped the known
pnpm 12.6.0 peer toggle on plugins/@sanity/debug-preview-url-secret-plugin
again; a second re-resolution (`pnpm dedupe`) undid it.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
#2046 added an `oxlint-disable-next-line react/exhaustive-effect-dependencies`
in ImagePaletteSection.tsx because the rule was on at the time. With the
rule off, the directive is reported as unused. Removing it also makes the
file match its last sanity-io/ui version apart from `type` imports.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Brings in the themer move and its Storybook in dev/storybook from #2046.

pnpm-workspace.yaml keeps both sides: main's @xstate/react and xstate
catalog entries next to this branch's vitest 5 ranges. The Storybook's
@vitest/browser-playwright goes from ^4.1.11 to ^5.0.2, ui's version,
since it peers on the exact vitest version.

pnpm-lock.yaml is main's lockfile regenerated with pnpm 12.6.0. The
regeneration kept @vitest/browser@4.1.11 and @vitest/runner@4.1.11 from
main's lockfile as optional peers of @storybook/addon-vitest, which
declares ^3 || ^4 (@vitest/runner has no v5). Removing those entries and
addon-vitest's snapshot and reinstalling re-resolves it against
@vitest/browser@5.0.2, as in ui's lockfile, so no vitest 4 package is
left. The second install also returned the known
@vitejs/devtools/oxc-transform-react toggle in
@sanity/debug-preview-url-secret-plugin to main's variant, so the diff
has no toggle. The Studio next versions stay at main's, and a clean main
with this branch's two manifest changes reproduces the same lockfile
with the same steps.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Brings in the @sanity/themer move from #2046. pnpm-lock.yaml is main's,
regenerated with pnpm 12.6.0 run directly. Versions only move for this
branch's toolchain (tsdown 0.23.0, @sanity/tsdown-config 0.28.2,
@sanity/pkg-utils 13.0.3, rolldown-plugin-dts 0.28.6 with its yuku 0.10
dependencies, @sanity/vanilla-extract-tsdown-plugin 0.4.2), and themer
now builds with them too. The rolldown-plugin-dts patch applies with the
same hash. `pnpm install --fix-lockfile` reverted the optional-peer
toggle of plugins/@sanity/debug-preview-url-secret-plugin that the
regeneration produced, so its entries match main's. The Studio stays on
6.17.0-next.108.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Brings in the themer move from #2046. Only pnpm-lock.yaml conflicted.

pnpm-lock.yaml is main's lockfile regenerated with pnpm 12.6.0, minus
the peer toggle AGENTS.md describes (debug-preview-url-secret-plugin's
sanity and @sanity/tsdown-config suffixes). It differs from main only by
this branch's own changes: oxfmt 0.71.0 with tinypool 2.2.0, and husky,
lint-staged and their exclusive dependencies removed.
sanity@6.17.0-next.108 doesn't move.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Requotes the three changesets from #2046
(dashboard-widget-vercel-xstate, shared-deps-sanity-ui-icons-motion and
themer-plugins-monorepo) now that the oxfmt preset no longer keeps
.changeset/*.md files on double quotes. Quote-only changes; the themer
package and dev/storybook already pass.
cursor Bot pushed a commit that referenced this pull request Sep 29, 2026
Brings in the Version Packages release (#2053), which consumed the three
changesets from #2046 that this branch had just requoted. The
modify/delete conflicts resolve to main's deletions. #2053 didn't
change pnpm-lock.yaml, so this branch's lockfile (main's, regenerated
with pnpm 12.6.0, minus the peer toggle) still applies. pnpm format
changes nothing.
stipsan added a commit that referenced this pull request Sep 29, 2026
…ommit hook on lefthook (#2047)

<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
Brings oxfmt in line with sanity-io/ui's `main`: the same version, the
same config, and the same scripts and CI wiring. The pre-commit hook
moves from husky and lint-staged to lefthook, set up like
sanity-io/sanity's. Supersedes #1919.

## Commits

1. **`chore(deps): update oxfmt to ^0.71.0`**: the catalog goes from
^0.63.0 to ui's ^0.71.0, with ui's oxfmt resolution: oxfmt 0.71.0 and
`tinypool` 2.2.0, the same versions and integrity hashes as ui's
lockfile. There's a patch changeset for `@sanity/plugin-kit`, whose
optional `oxfmt` peer uses the catalog.
2. **`chore(format): align the oxfmt config with sanity-io/ui`**: root
`oxfmt.config.ts` now ignores `storybook-static/**` and `CHANGELOG.md`,
like ui's `.oxfmtrc.json`. `.vscode/settings.json` points
`oxc.fmt.configPath` at `oxfmt.config.ts` instead of the `.oxfmtrc.json`
that #1565 deleted. AGENTS.md records the parity with ui and the
exceptions.
3. **`chore: drop the husky and lint-staged pre-commit hook, like
sanity-io/ui`**: superseded by commit 6, which brings the hook back on
lefthook.
4. **`chore(format): drop the oxfmt preset's .changeset/*.md override,
like sanity-io/ui`**: the `@sanity/plugin-kit/oxfmt` preset loses its
Prettier-era `singleQuote: false` override for `.changeset/*.md`, as
ui's `.oxfmtrc.json` did when ui switched Changesets to oxfmt formatting
(sanity-io/ui#2957). It gets a patch changeset for `@sanity/plugin-kit`,
since the preset is published. This part moved here from #2046 because
it's formatter config. AGENTS.md now explains how changesets get quoted.
5. **`chore(format): run pnpm format`**: requotes this PR's own
changeset from commit 1. Everything else already passed under oxfmt
0.71.
6. **`chore: run oxfmt on staged files with lefthook, like
sanity-io/sanity`**: see below.

## Pre-commit hook: husky and lint-staged → lefthook

This mirrors how sanity-io/sanity replaced husky and lint-staged
(sanity-io/sanity#12755, then sanity-io/sanity#12767 and
sanity-io/sanity#12953):

- **`lefthook` ^2.1.14** as a root devDependency, sanity's range. It
resolves to 2.1.14, as in sanity's lockfile.
- **`allowBuilds: lefthook: true`** in `pnpm-workspace.yaml`, as in
sanity. Lefthook's own install script runs `lefthook install` during
`pnpm install`, and it skips when `CI` is set, which covers GitHub
Actions and Vercel. There's no root `postinstall` script: sanity dropped
its `lefthook install --reset-hooks-path` postinstall in #12767 for this
package build.
- **`lefthook.yml`** with sanity's layout: a parallel `pre-commit` with
two jobs that run oxfmt on staged files with `stage_fixed: true`. The
jobs are `format-scripts` (`*.{js,jsx,ts,tsx,mjs,cjs,mts,cts}`) and
`format-other` (`*.{md,yml,yaml,css,json,jsonc}`).
- `mts`, `cts` and `jsonc` are additions to sanity's globs. They cover
this repo's `.mts`, `turbo.jsonc` and `knip.jsonc` files, which the old
lint-staged glob partly covered.
- sanity's `oxlint --fix` line is left to the oxlint alignment (#2049).
- **Direct binary calls:** the jobs call `node_modules/.bin/oxfmt`
instead of sanity's `pnpm oxfmt`. Running pnpm in a hook triggers pnpm
12's implicit install, which flips the lockfile's peer toggle (see
AGENTS.md). sanity switched to `pnpm oxfmt` in #12953 because lefthook
doesn't put `node_modules/.bin` on `PATH`; the path solves that too.
- **Lockfile:** `pnpm-lock.yaml` gains only lefthook 2.1.14 and its
platform packages (100 lines). A plain install would also have bumped
the Studio's `next` overrides from `6.17.0-next.108` to
`6.17.0-next.112`, so I resolved it through a local registry proxy that
served the locked versions as `next`, as AGENTS.md describes. The
lockfile has no `localhost` or `tarball:` entries.
- **Docs:** AGENTS.md documents the hook, `LEFTHOOK=0`, the one-time
reset for clones that ran husky, and `pnpm
--config.verify-deps-before-run=false <script>` for local runs. README's
note that formatting also runs in a pre-commit hook is back.

## Merges with `main`

`main` is merged in, not rebased, so nothing was force-pushed. After
each merge, `pnpm-lock.yaml` is `main`'s lockfile regenerated with pnpm
12.6.0, invoked directly so `packageManagerDependencies` stays, minus
the peer toggle that AGENTS.md describes.

- **#2039 (lock file maintenance):** only the lockfile conflicted.
- **#2048 (pnpm 12.6.0):** `package.json` conflicted next to the removed
`lint-staged` block. The resolution keeps both sides: `main`'s
`packageManager: pnpm@12.6.0` and `vercel` ^60.1.3, without the block. A
follow-up commit restores `main`'s side of the peer toggle, which the
regeneration had flipped.
- **#2046 (themer move):** only the lockfile conflicted. `pnpm format`
then requoted #2046's three changesets in a separate commit:
`dashboard-widget-vercel-xstate`, `shared-deps-sanity-ui-icons-motion`
and `themer-plugins-monorepo`, 45 frontmatter lines, quote-only. The new
`plugins/@sanity/themer` package and `dev/storybook` already passed.
- **#2053 (Version Packages):** it released and deleted those three
changesets, so the modify/delete conflicts resolve to `main`'s
deletions. It didn't change the lockfile, and `pnpm format` changes
nothing after the merge.

`pnpm-lock.yaml` differs from `main` only by this PR's own changes:

- oxfmt and its bindings go from 0.63.0 to 0.71.0, and `tinypool` from
2.1.0 to 2.2.0.
- husky, lint-staged and their four exclusive dependencies are removed.
- lefthook 2.1.14 and its platform packages are added.

No other version moves, and the Studio stays on `6.17.0-next.108`.

## How it compares with ui

- **Options:** identical, overrides included (`printWidth`, `semi`,
`singleQuote`, `bracketSpacing`, `quoteProps`, `sortImports`,
`sortPackageJson`). They come from the `@sanity/plugin-kit/oxfmt`
preset.
- **Ignore patterns:** `dist/**`, `storybook-static/**`,
`pnpm-lock.yaml` and `CHANGELOG.md` in both. Two differences remain:
  - `turbo/**/*.hbs` is plugins-only (the turbo generator templates).
- ui's `**/sanity.types.ts` is left out. In ui it was for
`apps/docs/sanity.types.ts` and has matched nothing since the docs app
went static. Here, `pnpm typegen` formats
`dev/test-studio/sanity.types.ts` on purpose: the test studio runs
typegen with `formatGeneratedCode: false` and then `pnpm format`.
Ignoring the file would make the next TypeGen PR unformat it.
- **Config file:** plugins keeps `oxfmt.config.ts` re-exporting the
published preset instead of ui's `.oxfmtrc.json`. #1565 made that switch
so the preset is exactly what the monorepo formats with, and
plugin-kit's `verify-package` expects plugins to use it. oxfmt, the Oxc
extension and Changesets' formatter detection all read
`oxfmt.config.ts`. See the open question.
- **Scripts and CI:** the `format` script is identical to ui's (`oxfmt .
--no-error-on-unmatched-pattern`). Neither repo checks formatting on
PRs, and both run the same shared `format-if-needed.yml` on `main`, so
no workflow changes are needed. `oxfmt .` also formats
`.github/workflows/*.yml`, which that workflow can't push, and 0.71
changes none of them. ui has no pre-commit hook; this repo keeps one, on
lefthook.
- **VS Code:** ui points `oxc.fmt.configPath` at its config file, but
plugins pointed it at a file that doesn't exist. I drove `oxfmt --lsp`
directly: with the old setting, the editor silently formats with oxfmt's
defaults (double quotes, semicolons, bracket spacing, unsorted imports),
and with `oxfmt.config.ts` it matches `pnpm format`.

## Changesets

- **Changesets formats what it writes with oxfmt.**
`.changeset/config.json` sets `"format": "oxfmt"` (from #2046). I called
`@changesets/write` (what `changeset add` uses) directly. With the
override it writes `"@sanity/plugin-kit": patch`; without it,
`'@sanity/plugin-kit': patch`, the same as `pnpm format`.
- **`changeset version` is unaffected.** It only hands `CHANGELOG.md`
files to the formatter. This PR ignores those, and oxfmt skips them and
exits 0 thanks to `--no-error-on-unmatched-pattern`.
- **`changeset status` parses the single-quoted frontmatter.** It lists
this PR's two changesets for `@sanity/plugin-kit` 10.0.11.

## What to expect after merging

- **Renovate changesets get a follow-up format PR, as in ui.** The
shared Renovate changesets workflow uses
`mscharley/dependency-changesets-action`, which always writes double
quotes and has no formatting option. After a Renovate PR merges, the
Auto format workflow opens (or updates) a `chore(format): 🤖 ✨` PR that
requotes the changeset. ui has had one after most Renovate merges since
sanity-io/ui#2957: sanity-io/ui#2971, sanity-io/ui#2980,
sanity-io/ui#2986, sanity-io/ui#2998, sanity-io/ui#3002,
sanity-io/ui#3013 and sanity-io/ui#3024.
- **Clones that ran husky need a one-time reset.** They still have
`core.hooksPath=.husky/_`. There, `lefthook install` refuses to install
and prints how to fix it, and `pnpm install` continues. Until the reset,
husky's leftover runner does nothing, so commits simply aren't
formatted. `git config --unset core.hooksPath && pnpm install` fixes it.

## Related PRs

- **#1919** (Renovate, oxfmt ^0.71.0): superseded. This PR has the same
bump, with the lockfile regenerated on current `main`, plus the config
alignment. Close it if Renovate doesn't once this merges.
- **#2046** (themer move) and **#2048** (pnpm 12): merged, and merged
into this PR.
- **#2049** (oxlint) conflicts with this PR in `pnpm-workspace.yaml`,
where its `oxlint` catalog lines sit next to `oxfmt`. Keep both sides.
It could also add sanity's `oxlint --fix` line to the `format-scripts`
job, calling `node_modules/.bin/oxlint` directly.
- **#2050** (vitest 5) conflicts only in `pnpm-lock.yaml`.
- **#2051** (tsdown) doesn't conflict.
- Whichever PR lands later merges `main` and regenerates the lockfile
without committing the peer toggle.

## What Cody needs to do

- Add the `🤖 bot` label: the agent's label tool couldn't edit PRs in
this repository.
- Triage Socket's alert. It flags `lefthook@2.1.14`'s install script
(`node postinstall.js`) as a low-priority warning. That script is what
installs the hook: `allowBuilds: lefthook: true` enables it, as in
sanity-io/sanity, and it skips when `CI` is set. Mark it as acceptable
risk if you agree. Socket's required checks pass either way.
- After merging, tell contributors to run `git config --unset
core.hooksPath && pnpm install` once, so lefthook replaces husky's hook.
- Merge the Renovate follow-up format PRs as they come, as in ui.
- No Vercel, secrets or ruleset changes, and no new secrets or env vars.

## Open question

**`.oxfmtrc.json`:** should plugins switch to ui's JSON config file,
too? That would mean no longer re-exporting the plugin-kit preset, and
it would reverse #1565. I kept `oxfmt.config.ts`, since the formatting
result is already the same as ui's.

## Verification

- **The hook**, tested on throwaway commits, with `pnpm-lock.yaml`
untouched each time:
  - Staged TS and JSON files get formatted and re-staged.
- A tracked `.vscode/settings.json` (its folder is gitignored) is
re-staged without the `[FAILED]` that lint-staged printed.
  - A staged `CHANGELOG.md`, which oxfmt ignores, passes as-is.
- In a partially staged file, only the staged hunk is formatted and the
unstaged hunk is kept.
- The commit that adds the hook ran it on its own files in 0.4 seconds.
- **Clones that ran husky:** with `core.hooksPath=.husky/_`, `lefthook
install` exits 1 with instructions and installs nothing, as described
above.
- **Local checks** at the head commit, with pnpm 12.6.0 on Node 24.21
and `CI=true` (lefthook's install script skips):
- `pnpm install --frozen-lockfile`, which leaves `pnpm-lock.yaml`
untouched
  - `oxfmt --check` (2,018 files)
  - `oxlint` and `knip`
  - the full turbo build (54 tasks)
  - `vitest run` (235 files, 1,509 tests)
  - `pnpm test:browser` (4 files, 7 stories)
- **CI** is fully green at the head commit (`123af257`): build, lint,
knip, all four test shards, coverage, `storybook-test`, both e2e
browsers, Bugbot, and both Vercel checks, whose installs run lefthook's
install script. The branch is up to date with `main`.
<!-- CURSOR_AGENT_PR_BODY_END -->

<div><a
href="https://cursor.com/agents/bc-0b8c4f47-d9af-5929-8254-d25847e7e2dd?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-web-light.png"><img
alt="Open in Web" width="114" height="28"
src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a>&nbsp;<a
href="https://cursor.com/background-agent?bcId=bc-0b8c4f47-d9af-5929-8254-d25847e7e2dd&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source
media="(prefers-color-scheme: light)"
srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img
alt="Open in Cursor" width="131" height="28"
src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a>&nbsp;</div>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
stipsan added a commit to sanity-io/themer that referenced this pull request Sep 30, 2026
…'s tooling (#379)

* chore: adopt sanity-io/ui's repo tooling

Switch to the same toolchain as sanity-io/ui: pnpm 12 with ui's workspace
settings and catalog, oxlint (type-aware, with type checking), oxfmt, knip,
TypeScript 7 and Changesets, replacing turbo, eslint, prettier and jest.

The lockfile is seeded from sanity-io/ui's, so the shared tooling resolves to
the same versions, while apps/v1 keeps its own pins. apps/v1 moves its tests
from jest to vitest (in the edge-runtime environment), its tsconfig from the
`baseUrl` TypeScript 7 removed to `paths`, and its Next.js config to ESM.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* refactor(v1): address the oxlint and knip findings

Remove unused imports, variables, exports, files and the unused culori
dependency, stray debug logging, and a redundant ternary. Type the snippet
compiler (which now writes its output with node:fs and formats it with oxfmt)
so it passes the type check, and render DiceIcon without forwarding React 18
typed props, which the React 19 typings elsewhere in the workspace reject.

None of this changes what the app renders or what /api/hues serves.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* feat(themer-legacy): move the package from sanity-io/ui

Copy packages/themer-legacy from the latest sanity-io/ui main, with its
version (0.2.1) and changelog, so the next release continues from the
latest published version. The package metadata now points at this
repository, and @sanity/color, @sanity/icons and @sanity/ui come from npm
(through the catalog) instead of the ui workspace. The build output is
byte-identical to the published 0.2.1.

The vanilla-extract plugin in its vitest config only compiled the ui
workspace's @sanity/ui source, so it goes.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* feat(storybook): add a Storybook for @sanity/themer-legacy

A React Storybook in apps/storybook, set up like sanity-io/ui's (addons,
decorator, Vitest browser tests in Chromium), in place of a workshop. The
theme/ stories preview every hosted preset through createTheme and
buildThemeFromUrl; the tool/ stories cover the hue editor, the tint strip and
the whole themerTool layout and navbar around a mock Studio, with play
interactions, plus a browser test that drafts survive a reload.

@sanity/ui comes from npm here, whose build doesn't import its own CSS, so
the preview imports @sanity/ui/styles.css instead of running ui's
vanilla-extract plugin.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* ci: adopt sanity-io/ui's workflows and Renovate config

CI, release, changesets-from-conventional-commits, format-if-needed,
pkg-pr-new and Renovate now match sanity-io/ui. Publishing moves from the
release job in ci.yml (npm token, workflow_dispatch) to release.yml, which
runs the Changesets action on main and publishes with npm trusted publishing
(OIDC). pkg-pr-new publishes packages/themer-legacy, without ui's unused
Turborepo env. Renovate leaves the @sanity/ui and @sanity/color that
apps/v1's /api/hues output is generated from alone.

The prettier and lock-threads workflows go: format-if-needed replaces the
former, and the latter has been failing on every run.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* docs: document the repository like sanity-io/ui

Add CONTRIBUTING.md and AGENTS.md, and repository, scripts and release
sections to the README, whose migration guide link now points at
packages/themer-legacy.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(format): format with oxfmt

Formatting only, as `pnpm format` does on main with the sanity-io/ui oxfmt
config: mostly apps/v1, which prettier formatted before.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* fix(v1): type-check and knip without the generated edge utils

/api/hues imports edge-utils/themeFromHuesTemplate.mjs, which build:edge
generates (before `next build` and `next dev`) and git ignores, so lint and
knip failed on a fresh checkout. A declaration in typings/ now stands in for
it through a paths fallback, while the generated module still wins once it
exists.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* fix(v1): commit the edge utils declaration

The unanchored `edge-utils` ignore also matched typings/edge-utils, so
anchor it to the directory build:edge generates.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(storybook): add sanity-io/ui's Storybook vercel.json

The same build command and output directory as ui's apps/storybook, for the
Vercel project that will deploy it.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* ci: install Playwright's system deps on a browser cache hit

The Playwright cache only restores the browsers, so on a hit the job never
installed the system packages `install --with-deps` adds on a miss. Same fix
as sanity-io/plugins#2046.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(themer-legacy): drop the babel-plugin-react-compiler typings stub

The package builds with the oxc React Compiler, and @sanity/tsdown-config's
types don't need the stub for defineConfig to resolve, so remove it and the
tsconfig include that only existed for it.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* Revert "chore(format): format with oxfmt"

This reverts commit f8e5a08.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: keep prettier as the formatter

oxfmt moves to its own PR. Until that lands, this branch formats like main
does today: prettier 3.2.5 with prettier-plugin-packagejson, main's
.prettierrc.cjs, .prettierignore and prettier workflow (set up with this
branch's pnpm 12 setup action), apps/v1's prettier config and a prettier
build:snippets, and Changesets formatting with prettier. The moved package
and the Storybook are left out of prettier, so they keep sanity-io/ui's
formatting.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: move knip to its own PR

Drop knip, its script, knip.jsonc and its CI job from this branch, and
leave what only knip asked for (removing apps/v1's dead modules, the unused
culori and prettier-plugin-packagejson dependencies, and exports only used
locally) to that PR: those files are back to how they are on main.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore: allow main's CommonJS prettier config in oxlint

.prettierrc.cjs stays exactly as it is on main until the repository moves
to oxfmt, so turn import/no-commonjs off for it.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(format): format with prettier

Formatting only: `pnpm format` with main's prettier config.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* docs: describe the prettier setup instead of oxfmt and knip

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* fix(v1): serve /api/hues from frozen bundles, byte for byte production's

The pnpm 12 lockfile resolves @sanity/color 3.0.8 and @babel/runtime 7.29.7
under @sanity/ui 2.1.1 and polished, where production bundles 3.0.5 and
7.24.4, and pnpm 12 names its node_modules/.pnpm folders differently, which the
?min=0 build prints as path comments. So every theme module /api/hues served
changed its bytes, if not its values. Pinning the two versions only fixes the
minified modules, so the esbuild bundles are now frozen instead: copies of the
build on main (b3dadf9), which reproduces every golden captured from production.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(v1): drop esbuild-plugin-replace, only the template build used it

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* docs: explain the frozen /api/hues bundles

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* docs: say what apps/v1's @sanity/ui 2 is for now

Its theme previews come from @sanity/themer-legacy, not from the @sanity/ui 2
source the app pins, which its UI and embedded Studios are built on.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

* chore(format): format with oxfmt

Formatting only: this branch's apps/v1 changes and config files in oxfmt's
style. packages/themer-legacy already had sanity-io/ui's formatting and
doesn't change, the Storybook only loses a blank line, and apps/v1/frozen/
isn't touched.

Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
Preview – plugins-studio — 5333ecae Deployed Sep 29, 2026 by vercel[bot]
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.

3 participants