[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components - #34831

Merged
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo
Oct 13, 2025
Merged

[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() Components#34831
eps1lon merged 2 commits into
react:mainfrom
eps1lon:sebbie/uee-memo

Conversation

@eps1lon

Copy link
Copy Markdown
Collaborator

Closes#34818

Seems to me the event payload handling should've gone into the original fallthrough block instead of just handling it for FunctionComponent: https://github.com/facebook/react/pull/25229/files#diff-9e8ee221dbf99e0ccdad6a63bf63804dea26f1785e2f443dbe92fa835a63b83eL412-L415

See first commit for current behavior where Effect Events did not read the latest Context value if used in React.memo Components.

@github-actionsgithub-actionsBot added the React Core Team Opened by a member of the React Core Team label Oct 13, 2025
Comment on lines +865 to +866
logContextValue = fireLogContextValue;
}, []);

Copy link
Copy Markdown
CollaboratorAuthor

Choose a reason for hiding this comment

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

The original report registered an interval here. Even after rendering with a new context value, the interval continued to log the initial value.

@react-sizebot

Copy link
Copy Markdown

Comparing: 1d68bce...6bec011

Critical size changes

Includes critical production bundles, as well as any change greater than 2%:

Name+/-BaseCurrent+/- gzipBase gzipCurrent gzip
oss-stable/react-dom/cjs/react-dom.production.js=6.68 kB6.68 kB=1.83 kB1.83 kB
oss-stable/react-dom/cjs/react-dom-client.production.js=605.42 kB604.97 kB=107.21 kB107.14 kB
oss-experimental/react-dom/cjs/react-dom.production.js=6.69 kB6.69 kB=1.83 kB1.83 kB
oss-experimental/react-dom/cjs/react-dom-client.production.js=664.39 kB663.94 kB=117.09 kB117.03 kB
facebook-www/ReactDOM-prod.classic.js=688.26 kB687.80 kB=121.13 kB121.07 kB
facebook-www/ReactDOM-prod.modern.js=678.69 kB678.22 kB=119.48 kB119.42 kB

Significant size changes

Includes any change greater than 0.2%:

(No significant changes)

Generated by 🚫 dangerJS against 6bec011

@eps1loneps1lon changed the title [Fiber] Ensure Effect events read latest values in forwardRef and memo() Components[Fiber] Ensure useEffectEvent reads latest values in forwardRef and memo() ComponentsOct 13, 2025
@eps1lon
eps1lon marked this pull request as ready for review October 13, 2025 08:08
@eps1lon
eps1lon merged commit 93d4458 into react:mainOct 13, 2025
249 of 250 checks passed
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
github-actionsBot pushed a commit that referenced this pull request Oct 13, 2025
@eps1lon
eps1lon deleted the sebbie/uee-memo branch October 13, 2025 16:05
sebmarkbage pushed a commit to vercel/next.js that referenced this pull request Oct 14, 2025
SaiKarthikYamavaram pushed a commit to SaiKarthikYamavaram/react that referenced this pull request May 15, 2026
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 30, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
stipsan added a commit to sanity-io/ui that referenced this pull request Jul 31, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 2, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 5, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 6, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
@controversial

Copy link
Copy Markdown

@eps1lon any hope of getting this picked into a stable release? Judging by the amount of mentions in this thread, and the comments on #35187, it seems like this blocks quite a few people from adopting useEffectEvent

cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
…ss contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
cursorBot pushed a commit to sanity-io/ui that referenced this pull request Aug 7, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/ui that referenced this pull request Aug 10, 2026
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): ship dist/styles.css unminified for diffable CSS output (#2528)
* fix(ui): preserve overflow: hidden fallback in dist/styles.css
lightningcss's OverflowHandler collapses duplicate overflow declarations
to the last value no matter which targets it is given (unlike colors, it
never consults compat data for the clip keyword), so no browser target
setting can keep the hand-written fallback. Skip the lightningcss pass
instead (minify: false + target: false) so the stylesheet ships exactly
as authored, keeping overflow: hidden ahead of overflow: clip for
browsers in the support matrix without clip support (e.g. iOS Safari 15).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: format spinner.tsx (pre-existing oxfmt drift)
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(ui): keep lightningcss lowering, only disable minification of styles.css
overflow: clip is Baseline, so the hidden fallback doesn't need to
survive the build. Keep minify: false so CSS output diffs easily between
published versions, and drop target: false so the lightningcss pass runs
again with the @sanity/browserslist-config lowering targets.
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>
* chore: ban class components via react/prefer-function-component (#2525)
* chore: enter changesets prerelease mode on next
Enter the `next` prerelease tag, point Changesets and CI/release
workflows at the `next` branch, and add a sample major changeset for
@sanity/ui so the Version Packages bot can open the first pre-release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2504)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat(ui): tombstone deprecated props, hooks, and components (major) (#2509)
* chore(ui): tombstone remaining deprecated props as never
Remove runtime fallbacks for deprecated space, grid, MenuButton
popover, Popover boundaryElement, Badge mode, and Avatar tone props.
Defaults move onto their replacements (gap, gridTemplate*, popover).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix(storybook): migrate deprecated space/columns/useElementRect props
Replace Stack/Inline/Menu/Tree/TabList/Button `space` with `gap`, Grid
`columns`/`rows` with `gridTemplateColumns`/`gridTemplateRows`, and
`useElementRect` with `useElementSize` (border size). Drop matching
no-deprecated suppressions; leave theme/_compat suppressions alone.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(ui): tombstone deprecated APIs with never types (major)
Finish call-site migrations, add throwing-hook tests, and a major
changeset documenting removed props/hooks/components and their
replacements.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert(ui): keep PortalProvider boundaryElement for PortalContext
PortalProvider.boundaryElement still writes PortalContextValue, so it
cannot be tombstoned like other deprecated props.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop PortalProvider from tombstone changeset notes
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>
* Version Packages (next) (#2512)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat: React 19 compiler target and ^19.2 peer deps (#2507)
* feat: target React 19 compiler and require react ^19.2
Compile @sanity/ui and @sanity/themer with reactCompiler target '19',
drop the now-unnecessary react-compiler-runtime dependency, and tighten
react (and react-dom for ui) peer ranges to ^19.2 across published packages.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: mark @sanity/themer bump as major in changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave @sanity/icons and @sanity/logos peer ranges unchanged
Only @sanity/ui and @sanity/themer need the React 19.2 peer bump for the
compiler target change.
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>
* feat(ui): remove private `_visual-editing` entrypoint (#2513)
* feat(ui): remove private `_visual-editing` entrypoint
Drop `@sanity/ui/_visual-editing` now that tree-shaking from the main
export is sufficient for visual-editing consumers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: stop deep-linking workspace packages into transitive deps
Set linkWorkspacePackages to true (not deep) so sanity and
@sanity/visual-editing keep registry @sanity/ui until those
packages are updated for the removed _visual-editing entrypoint.
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>
* fix(ui): restore Popover max-width after boundaryElement migration (#2514)
fix(ui): restore Popover max-width from floatingBoundary
After tombstoning `boundaryElement`, max-width only came from
BoundaryElementProvider, so migrating to floatingBoundary/referenceBoundary
alone dropped constrainSize/preventOverflow width. Fall back to the floating
boundary for size, and wrap the Aligned story in BoundaryElementProvider.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2515)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* feat!: drop CommonJS from @sanity/ui, require Node.js >=22.12 (#2510)
* feat!: ship ESM only and require Node.js >=22.12
Drop CommonJS build outputs and `require` export conditions from
@sanity/ui, @sanity/color, and @sanity/logos. Raise published package
engines to >=22.12 to match sanity.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: leave color, logos, and icons unchanged
Scope the ESM-only / engines bump to @sanity/ui and @sanity/themer only.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): drop custom chunkFileNames, use tsdown defaults
Hashed shared-chunk filenames already prevent the entry/chunk
collision that dist/_chunks/ was guarding against (#2262).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): restore UserConfig annotation on tsdown config
Match themer/logos so the inferred config type stays portable in the
package TypeScript program (TS2883).
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>
* Version Packages (next) (#2516)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix: revert @sanity/themer to last published prerelease
* Revert "fix: revert @sanity/themer to last published prerelease"
This reverts commit e325e53.
* feat: replace AnimatePresence with AnimateActivity (#2508)
* feat: replace AnimatePresence with AnimateActivity
Vendor Motion's AnimateActivity (Activity + exit animations) and use it
in Tooltip, Popover, and Toast. Requires React 19.2 (handled separately).
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimateActivityProps module-private for knip
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: keep AnimatePresence for ToastProvider
Toasts are mount/unmount list items; AnimatePresence fits that better
than Activity-based show/hide. AnimateActivity stays for Tooltip/Popover.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lazy-mount AnimateActivity until first open
Keep tooltips/popovers out of the DOM until first shown so hidden
Activity nodes do not leak duplicate text into Testing Library queries
(e.g. delay-group stories). After first open, Activity still preserves
exit animations and state on subsequent hides.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: drop lazy-mount ref, assert tooltip visibility in stories
Hidden Activity content is intentionally pre-rendered in the DOM, so the
tooltip stories wait for visibility instead of text presence. Flipping
animate off while open skips the exit animation, which is acceptable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: await async expect in tooltip story play functions
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat: keep closed tooltips/popovers mounted with Activity
Wrap the animate=false paths in plain <Activity> instead of unmounting,
so open/close preserves state in both modes. Reset the menu's active
item via effect cleanup (Activity unmounts effects on hide) so
shouldFocus and selected registration behave like a fresh mount on
reopen. Update tooltip/menu tests to assert visibility rather than DOM
presence, and gate the Recursive popover story's content on open since
hidden activities pre-render (an unconditional recursive content would
render an infinitely deep hidden tree).
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>
* chore: document why native useEffectEvent is blocked, pin the freshness contract (#2506)
* feat!: use React's native useEffectEvent
Remove the use-effect-event dependency in favor of React's built-in
useEffectEvent, and raise the react/react-dom peer dependency minimum
to ^19.2.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: route useEffectEvent through an internal shim
React 19.2's native useEffectEvent never updates past first-render
values when the calling component is wrapped in forwardRef or memo
(react/react#34818) — the commit phase only applies effect-event
updates to plain function-component fibers. DialogCard and Tooltip are
forwardRef components, so Escape handling read a stale isTopLayer and
one Escape cascade-closed every nested dialog.
Keep the use-effect-event dependency removed, but route the three call
sites through a small internal shim (useRef + useInsertionEffect) until
react/react#34831 ships in a stable release.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: lint errors in useEffectEvent test, update changeset
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: update changeset for internal effect-event shim
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* revert: keep use-effect-event, native useEffectEvent unusable in forwardRef/memo
Audit of all call sites: useGlobalKeyDown and useClickOutsideEvent are
public hooks that execute in the caller's fiber (all internal callers —
DialogCard, Menu, Breadcrumbs — are forwardRef components, and external
callers can be anything), and Tooltip is a forwardRef component. React
19.2's native useEffectEvent never updates past first-render values in
forwardRef/memo fibers (react/react#34818), so no site can use the
native hook until the fix ships in a stable release.
Restore the use-effect-event dependency at all three sites with TODO
comments, drop the internal shim, and add a regression test that pins
the fresh-values contract for forwardRef/memo callers.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: restore lockfile to match next
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>
* feat(ui)!: drop @juggle/resize-observer, use native ResizeObserver (#2518)
* fix(ui): drop @juggle/resize-observer, use native ResizeObserver
ResizeObserver is available in all browsers Sanity UI targets, so the
polyfill and its custom typings are no longer needed.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* refactor(ui): remove _ResizeObserver wrapper, use native ResizeObserver directly
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>
* feat: ban forwardRef, createRef, createElement, Component, PureComponent with oxlint (#2519)
feat: ban forwardRef, createRef, createElement, Component, PureComponent via oxlint
Adds react entries to eslint/no-restricted-imports in .oxlintrc.json and
migrates every @sanity/ui component from forwardRef to React 19 ref-as-prop
function components. @sanity/icons and @sanity/logos keep forwardRef through
a scoped override since their react peer ranges still include React 18,
where refs only attach to forwardRef-wrapped function components. The class
ErrorBoundary keeps an inline disable (error boundaries have no function
equivalent), and the useGlobalKeyDown freshness-contract test keeps its
deliberate forwardRef usage with an inline disable.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore(deps): update renovatebot/github-action action to v46.2.0 (next) (#2521)
* Version Packages (next) (#2517)
* feat(ui): move heavy-dependency components to dedicated subpath entry points (#2522)
* feat(ui): extract static CSS with vanilla-extract into @sanity/ui/styles.css (#2523)
Sets up the vanilla-extract build pipeline in @sanity/ui via
@sanity/tsdown-config's vanillaExtract option, with inject disabled so the
stylesheet is never self-imported: consumers add
`import '@sanity/ui/styles.css'` themselves, on the same export path the
vanilla-extract based next major already uses.
Migrates the first slice of fully static styled-components CSS (SrOnly,
Spinner, SpanWithTextOverflow) to colocated .css.ts modules, and wires
.css.ts compilation into every consumer of the package's TypeScript source:
storybook, the icons app, the ui/themer vitest configs, the docs studio
(sanity dev/build), and the docs Next app (Turbopack, via
@vanilla-extract/next-plugin).
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: ban class components via react/prefer-function-component
Replace the no-restricted-imports ban on Component/PureComponent with
react/prefer-function-component (allowErrorBoundary), so ErrorBoundary
no longer needs an inline disable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2524)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
* refactor(ui): use function declarations instead of named const expressions (#2526)
Replace `export const Foo = function Foo` (and matching internal helpers)
with `export function Foo`. Leave polymorphic `XxxComponent = function Xxx`
casts as-is so display names and type assertions keep working.
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Version Packages (next) (#2529)
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
* fix(ui): emit types for styles css (#2536)
* Version Packages (next) (#2538)
* chore: exit changesets prerelease mode, restore main release config
Prep for merging the 4.0 branch to main: `changeset pre exit` (the next
`changeset version` on main produces stable 4.0.0), point the Changesets
baseBranch back at main, drop the `next` push triggers from the CI and
release workflows, and delete the placeholder kickoff changeset that
only existed to start the prerelease cycle.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: make pending changesets easier to scan
Rewrite each pending 4.0 changeset with a bold one-line takeaway first,
short bullets for what breaks, and an explicit what-to-do section, so
the stable 4.0.0 changelog is skimmable.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* feat(docs): serve the docs site from the v4 nav document
Swap primaryNavId from 'v3' to 'v4' so /ui renders the v4 nav. The
stale "UI4" content release (rzyecOZ0D, staged against the legacy main
nav in Oct 2025) is archived in the dataset, replaced by a fresh empty
"UI 4" release (rG3Odb604) for staging the v4 content updates.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: add v3-to-v4 migration guide, link it from the docs navbar
MIGRATION.md walks through the 4.0 upgrade as an ordered checklist
(runtime requirements, styles.css import, moved entry points, removed
deprecated APIs, behavior changes, themer 1.0). The docs navbar links
to it on GitHub next to the repository button.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: keep @sanity/themer on the 0.x line, bumping minor to 0.3.0
Downgrade the themer bumps in the pending changesets from major to
minor. Exiting pre mode increments from the current package version, so
reset packages/themer/package.json from the 1.0.0-next.7 prerelease to
the published stable 0.2.1 - `changeset version` now produces 0.3.0
(verified with a dry run). The migration guide section is retitled
accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the styles.css declaration changeset
The declaration fix only ever shipped on 4.0.0-next.* test releases -
v3 has no styles.css, so it is noise in the stable 4.0.0 changelog. The
declaration itself still ships with the build.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: require('@sanity/ui') keeps working on modern Node
Dropping the CJS build does not break require() consumers: the root
export resolves via a default condition and Node >=22.12 supports
require(esm). Only a too-old Node version breaks. Correct the changeset
and migration guide accordingly.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: split multi-package changesets per package
Give @sanity/ui and @sanity/themer their own changesets for the ESM /
Node 22.12 requirement, the React 19.2 requirement, and the subpath
entry point republish. Most @sanity/ui consumers have no relationship
to @sanity/themer, so cross-package mentions in the changelog are
noise. Verified with a dry-run version: bumps unchanged (ui 4.0.0,
themer 0.3.0) and the ui changelog no longer mentions themer.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop the unminified styles.css changeset
Like the declaration changeset, this only made sense within the
4.0.0-next.* test releases: v3 has no styles.css, so for anyone
upgrading from v3 the stylesheet has always been unminified.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css without assuming future majors
Drop the claims that more styles move into the stylesheet over time and
that the export path matches a vanilla-extract-based next major - the
changeset and migration guide should only describe what UI4 ships.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: point at the v3 maintenance branch
A copy of main now lives on the v3 branch (its release-v3 dist-tag
setup lands in #2551). Document it where the 4.x line talks about
versions: the migration guide points people staying on 3.x at it, and
CONTRIBUTING/AGENTS describe the maintenance-branch and dist-tag layout
alongside the existing v2 line.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: regenerate lockfile after rebasing onto main
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: incorporate the v2-to-v3 migration guide
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: combine the 4.0 changesets into a single release note
Merge the nine @sanity/ui changesets into one major changeset so the
4.0.0 changelog reads as a single coherent release note (linking the
migration guide), and the three @sanity/themer changesets into one
minor changeset. Delete the leftover pre.json - without preState the
version math is unchanged (verified by dry run: ui 4.0.0, themer
0.4.0). The dependency-update changesets inherited from main are left
as-is.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exclude @sanity/themer from the 4.0 release
Keep the published @sanity/themer exactly as main ships it: restore its
react peer range, engines, react-compiler-runtime dependency, compiler
target 18 (and the matching knip ignore entry), drop its changeset and
the migration guide section, and add it to the Changesets ignore list
so the @sanity/ui major does not auto-bump and republish it as a
dependent. Only in-repo dev shims remain (subpath imports of the
workspace ui and the vanilla-extract vitest plugin), which the monorepo
needs to build and test themer against ui v4 source. A follow-up PR
releases themer against ui 4 and removes the ignore.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: explain why React 19.2 is the minimum version
The 19.2 floor comes from the <Activity> component, which shipped in
React 19.2.0 and which v4 uses for Tooltip/Popover mounting. State this
in the release note and the migration guide.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: describe styles.css as a move off styled-components
Consumers do not need to know the build-time tooling. Say what matters
to them: these components no longer use styled-components, more follow
in future 4.x minors, and UI5 ships without runtime CSS-in-JS.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: point MenuButton boundaryElement deprecation at the v4 Popover API
The tombstone said popover={{boundaryElement}}, but PopoverProps.
boundaryElement is itself removed in v4 - the working replacements are
floatingBoundary/referenceBoundary. Also correct the _visual-editing
migration note: the moved components (Menu*, Popover) import from their
new entry points, not the root. Both spotted in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* docs: state the exports-aware moduleResolution requirement
Dropping typesVersions means moduleResolution "node" (node10) cannot
resolve the subpath entry points' types - the root entry still resolves
via the kept top-level types field. Document that v4 needs node16,
nodenext or bundler. Raised in review by @jordanl17.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* Update config.json
* Update queries.ts
* Update constants.ts
* Update avatar.tsx
* Update button.tsx
* Update card.tsx
* Update code.tsx
* Update container.tsx
* Update flex.tsx
* Update grid.tsx
* Update heading.tsx
* Update inline.tsx
* Update kbd.tsx
* Update label.tsx
* Update stack.tsx
* Update text.tsx
* Update CHANGELOG.md
* Update package.json
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
Co-authored-by: squiggler-app[bot] <265501495+squiggler-app[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
stipsan added a commit to sanity-io/use-effect-event that referenced this pull request Aug 27, 2026
…ffectEvent identity-stable (#106)
* chore: replace eslint and prettier with oxlint and oxfmt
Mirrors sanity-io/react-rx: .oxlintrc.json with type-aware linting via
oxlint-tsgolint (typeCheck replaces a separate tsc pass in CI later),
.oxfmtrc.json matching the @sanity/prettier-config house style, and the
reusable sanity-io format-if-needed workflow replacing prettier.yml.
Test files get a scoped rule override: the suite deliberately violates
the effect-event usage contract (render-phase calls, identity capture,
handler calls) to pin the ponyfill's runtime semantics.
Also renames the deprecated vitest `workspace` option to `projects`
(supported since vitest 3.2), which oxlint's typescript/no-deprecated
caught.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: migrate build from @sanity/pkg-utils to tsdown
Follows the sanity-io/react-rx setup: @sanity/tsdown-config drives
tsdown (dual ESM+CJS output preserved, exports map generated with
devExports pointing at source locally and dist in publishConfig),
@sanity/tsconfig/strictest replaces the pkg-utils tsconfig presets via
the tsconfig.base/tsconfig.build split, and TypeScript moves to 7.0.2.
Test infra moves with it: vitest 4 (projects API), @vitejs/plugin-react
6, jsdom 30, react 19.2.8 at the root, and the react-experimental
workspace bumps its May-2025 pin to the current experimental build
(experimental_useEffectEvent is now the stable-named useEffectEvent).
plugin-react stopped setting resolve.dedupe in v5, so the per-workspace
react pins now declare it themselves - without it the shim resolved the
root react while react-dom used the workspace copy.
New react-compiler vitest project runs a dedicated compiler-safe suite
through the oxc React Compiler (oxc-transform-react), the way compiled
consumers use this package. Its components live at module scope because
the compiler hoists non-reactive callback captures to module scope - an
upstream bug in babel-plugin-react-compiler@1.0.0 that oxc ports
faithfully (verified by transforming the same source through both) -
which breaks components created inside factories. A test.fails pin
documents that bug and will flag the release that fixes it.
pnpm 11, with .npmrc settings moved into pnpm-workspace.yaml.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: replace semantic-release with changesets
Mirrors sanity-io/react-rx: .changeset/config.json with the GitHub
changelog generator, a release.yml workflow that calls the reusable
sanity-io changesets workflow (changesets/action publishing via
'pnpm release' with npm provenance through OIDC trusted publishing),
and the bot-gated workflow that derives changesets from conventional
commits so renovate bumps keep releasing.
ci.yml loses the semantic-release job and its workflow_dispatch
release input - releases now flow from merged changesets on main.
renovate.json drops the eslint-plugin-react-hooks experimental-tag
rule (the dependency is gone) and groups oxlint with oxlint-tsgolint.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* fix: return a stable function reference from useEffectEvent
The returned function is now created once per component instance
(useState lazy initializer) and reads the latest callback through the
existing insertion-effect ref, so it can never grow stale. Previously a
new closure was returned on every render, matching the native hook.
Why stable identity is the right call, verified empirically:
- oxlint's compiler-ported react/exhaustive-effect-dependencies rule
only exempts the native useEffectEvent (import-source aware) and
requires this ponyfill's functions in effect dependency arrays, while
eslint-plugin-react-hooks@7 and oxlint's own exhaustive-deps forbid
exactly that (name-based matching). With a stable identity the
contradiction stops mattering at runtime: inclusion no longer
re-fires the effect on every render.
- The React Compiler (babel-plugin-react-compiler@1.0.0 and the oxc
port produce byte-identical output for this package) never rewrites
dependency arrays, but memoizes effect callbacks keyed on the event
function's identity - stable identity turns those into cache hits.
New test coverage documents the React 19.2 native useEffectEvent bug
where effect events read first-render values forever inside React.memo
and React.forwardRef components (react/react#34818, the bug that
keeps sanity-io/ui and radix-ui on ponyfills):
- test/react-19.2 (new, pinned to the 19.2 line via renovate) asserts
the native staleness as documented behavior and proves the ponyfill
stays fresh in the same scenarios.
- test/react-experimental verifies react/react#34831 fixed the
native hook in current experimental builds, and splits the identity
expectations (native: new function per render; ponyfill: stable).
- react 18/19/compiler projects pin ponyfill freshness under memo and
forwardRef, and that effects keyed on the event function fire once.
The workspace root joins pnpm-workspace.yaml packages so changesets can
version the package; ships with a patch changeset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop experimental changesets options
updateInternalDependents/onlyUpdatePeerDependentsWhenOutOfRange only
affect workspaces with internal dependents - this repo publishes a
single package and the test workspaces don't depend on it.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: enforce isolated declarations in the build tsconfig
Restores what tsconfig.dist.json had under pkg-utils, via the
@sanity/tsconfig/isolated-declarations preset.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: exempt react packages from the release-age cooldown without pinning
Experimental react builds are cut daily, so version-pinned excludes go
stale on every renovate bump.
Co-authored-by: Cody Olsen <stipsan@users.noreply.github.com>
* chore: drop 'experimental' from the npm package description
The hook is stable since React 19.2; keeps npm metadata consistent
with the README.
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>
jfolcini added a commit to jfolcini/agaric that referenced this pull request Aug 27, 2026
…t changed behaviour (#4465)
* refactor(lint): clear 25 of the react(refs) findings, and say why the other 69 stay
Refs #4406
`react(refs)` measured 94 on `main` (the issue says 96; it had already drifted
before this branch). This takes it to 69.
The bulk of what moved was one pattern, found by reducing it to a throwaway
repro rather than guessed at: oxlint flags ANY member expression passed
directly to a JSX `ref=` or handler prop as a render-phase ref access, whether
or not the property is a ref -- the rule is syntax-only and cannot see through
the hook-return boundary. Destructuring the hook's return into local
identifiers first satisfies it with zero behaviour change. That accounts for 18
findings across `SelectionBubbleMenu`, `ImageResizeToolbar`, the emoji
picker's `SkinToneSelector`, and the roving-tabindex test harnesses.
The one genuine fix is `useLocalStoragePreference`, whose render-facing return
read `failedWriteRef.current` directly. The ref now keeps only the job it is
needed for -- synchronous freshness inside `setPreference`, where two calls in
the same tick must not both read a value captured at last render -- while the
returned value comes from real state. Verified by falsification rather than
inspection: neutering the render-facing line drives `a failed write degrades to
in-memory` to a verbatim RED, and the restore is `cmp`-identical.
## Why this does not reach zero
The remaining 69 across 15 files are all shapes the repo has already ruled on,
in `docs/architecture/frontend.md` "Latest-value mirrors" or in #4398, and each
now carries an in-code reason citing this issue:
- a ref written and read within the SAME render (a `useMemo`-scoped cache) --
the doc's explicit "different hazard, not a mirror candidate" bucket;
- React's own documented lazy-ref-initialisation idiom
(`if (!ref.current) ref.current = ...`);
- a ref handed to a consumer that defers the mutation -- TipTap `.configure`
closures, ref-callback registration -- where `useEffectEvent` is not
available as the fix;
- a render prop invoked during render, receiving a ref-touching callback.
`useEffectEvent` was deliberately NOT reached for. It is silently broken under
`memo()`: the fiber-tag switch in the shipped bundle is `case 11: case 15:
break;`, so `ForwardRef` and `SimpleMemoComponent` fall through without wiring
the event. Fixed upstream by react/react#34831 but excluded from 19.2.1 and
not in any stable release (#4397 tracks it here).
Five findings are suppressed rather than restructured, both in test-support
files that already carried an `oxlint-disable` citing #4409 for a sibling line
of the identical hazard; extending the existing treatment beat leaving it half
suppressed. No suppression is bare -- each states its reason.
So the rule is NOT ready to be promoted to error, which was the issue's stated
end goal. What is left is a judgement about whether the four patterns above
should be permanently exempted at config level or individually annotated
forever; that decision is not this PR's to make.
Verification: 94 -> 69 by `npx oxlint 2>&1 | grep -c 'react(refs)'`, no new
findings in any other rule (full before/after diff), `npx tsc -b` clean, 674
tests across the 24 affected suites passing.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): drop the useLocalStoragePreference change, which reds the mobile mermaid e2e
`validate / playwright (2)` fails reproducibly on this branch — four runs,
including two CI retries — on `mobile-editor.spec.ts`'s "a mermaid code block
renders and stays responsive on the mobile user agent". Typed source never
reaches the block; it stays on "Empty diagram — switch to source".
It is this branch, not `main` and not a flake: #4463 and #4464 both pass that
same shard from the same base.
## Why this change and not the others
Of the 13 files here, only 6 carry a non-comment change, and 3 of those are the
identical mechanical destructuring (`const { containerRef, ... } =
useRovingTabindex()` in place of member access on the hook's return) with no
behavioural delta. One is a test mock. `useLocalStoragePreference` is the only
production change with real logic in it.
And it has a plausible mechanism. The old code bumped state ONLY on a failed
write; the version added here also calls `setFailedWriteState(null)` on EVERY
SUCCESSFUL write. `MermaidDiagram`'s node view is documented in that very spec
as remounting asynchronously on re-key, so an extra render on the preference
path is exactly the shape that breaks it.
I have not proved the mechanism — the local repro was blocked on a missing
Playwright browser, and the first local run I did get "failed" in 2ms on a
`browserType.launch` ENOENT, which is an infrastructure error and not a
reproduction of anything. Rather than assert a cause I could not measure, this
removes the only candidate and lets CI answer.
So the 7 `react(refs)` findings in `useLocalStoragePreference` stay open. That
change deserves its own PR and its own e2e run regardless: it is the one edit
here that alters render behaviour, and the reviewer of #4465 flagged it as the
one thing to read closely.
Count moves 94 -> 76 rather than 94 -> 69.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
* refactor(lint): revert the SelectionBubbleMenu destructuring, which breaks the mobile mermaid e2e
Bisected, not guessed. Same machine, same test, same environment:
main PASS (981ms)
branch FAIL (15s timeout)
branch, all three destructurings reverted PASS
+ SelectionBubbleMenu restored alone FAIL
+ ImageResizeToolbar also restored FAIL
branch, only SelectionBubbleMenu reverted PASS
So `SelectionBubbleMenu.tsx` is the cause, alone and sufficiently.
## What I do NOT know
Why. The change reads as behaviour-identical -- `const { containerRef, onKeyDown,
onFocus } = useRovingTabindex()` in place of member access on the same call's
return -- and that destructured form is what `useRovingTabindex`'s own docblock
gives as the documented usage. The hook returns `useCallback`/`useRef` values,
not getters, so eager evaluation is not obviously the difference.
The plausible candidate is the React Compiler: this repo compiles with it, and
changing how a value is read changes what the compiler can prove about
dependencies, hence what it memoises. `BubbleMenu` is a TipTap component whose
node views the failing spec documents as remounting asynchronously on re-key.
That is a hypothesis. I have not verified it, and this PR does not claim it.
Filed as a follow-up with the reproduction, because "a cosmetic destructuring
changed runtime behaviour" is worth understanding rather than routing around --
and if the mechanism is the compiler, it applies to every other site the
`react(refs)` sweep would touch the same way.
## What this cost me
I twice told the reviewer-facing record this failure could not be from this
diff, reasoning from the file list: 13 files, only 6 with non-comment changes,
none touching mermaid or the editor node views. Every fact in that argument was
true and the conclusion was false. I also first "reproduced" it locally against
a Playwright install with no browser -- a 2ms `browserType.launch` ENOENT that
looks exactly like a test failure in a summary line.
Both are the same error as the ones this session has been fixing in the
codebase: an argument that explains the evidence is not the same as a
measurement, and an empty or absent result is not a passing one.
## Net
`react(refs)` 94 -> 78. The `useLocalStoragePreference` change stays out (it
was reverted on a hypothesis that proved wrong, but it is the one edit here with
real render-behaviour delta and the #4465 reviewer flagged it for close
reading -- it deserves its own PR and its own e2e run). `SelectionBubbleMenu`'s
7 findings stay open pending the follow-up.
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
---------
Signed-off-by: Javier Folcini <jfolcini86@gmail.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedReact Core TeamOpened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Stale closures with useEffectEvent

4 participants

@eps1lon@react-sizebot@controversial@sebmarkbage