diff --git a/.storybook/global.css b/.storybook/global.css
index 0ca6719f3..953125c40 100644
--- a/.storybook/global.css
+++ b/.storybook/global.css
@@ -2,7 +2,13 @@ html {
background: var(--vscode-sideBar-background);
}
+/* Typography lives on body like real webviews, so portalled content
+ (menus, tooltips) inherits it too. */
body {
+ color: var(--vscode-editor-foreground);
+ font-family: var(--vscode-font-family);
+ font-weight: var(--vscode-font-weight);
+ font-size: var(--vscode-font-size);
margin: 0;
padding: 0;
overflow: hidden;
@@ -15,10 +21,6 @@ body {
#root {
overscroll-behavior-x: none;
background-color: var(--vscode-sideBar-background);
- color: var(--vscode-editor-foreground);
- font-family: var(--vscode-font-family);
- font-weight: var(--vscode-font-weight);
- font-size: var(--vscode-font-size);
margin: 0;
max-width: 100%;
/* arbitrary size choice for the rough VSCode sidebar size */
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 8198aae6b..13081916f 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -208,6 +208,25 @@ export default defineConfig(
},
},
+ // Keep the UI package independent from other workspace packages.
+ {
+ files: ["packages/ui/**/*.{ts,tsx}"],
+ rules: {
+ "import-x/no-relative-packages": "error",
+ "no-restricted-imports": [
+ "error",
+ {
+ patterns: [
+ {
+ group: ["@repo/*"],
+ message: "packages/ui must not import other workspace packages.",
+ },
+ ],
+ },
+ ],
+ },
+ },
+
// React rules with type-checked analysis (covers hooks, JSX, DOM)
{
files: ["packages/**/*.{ts,tsx}"],
diff --git a/packages/ui/README.md b/packages/ui/README.md
index 04b76f5bf..4e4d4028a 100644
--- a/packages/ui/README.md
+++ b/packages/ui/README.md
@@ -23,14 +23,37 @@ import "@repo/ui/tokens.css";
`codicon.css` re-exports the codicon font and classes.
+## Overlays
+
+`Tooltip`, `ContextMenu`, and `DropdownMenu` wrap the Radix primitives and
+are styled to match the native VS Code menu and hover widgets. The menus
+expose Radix's compound parts as flat named exports
+(`DropdownMenuTrigger`, `DropdownMenuItem`, …); `Tooltip` is a single
+component taking a `content` prop, with a 500ms show delay matching VS
+Code's `workbench.hover.delay` default. Each `Tooltip` mounts its own
+Radix provider, so the cross-trigger skip-delay window is not shared
+between tooltips; if that matters, expose a shared provider. Checkbox/
+radio items, labels, and keybinding hints are not wrapped yet.
+
+Overlay content is portalled to `body` and inherits webview typography
+from there; surface colors come from the `--ui-menu-*` and
+`--ui-tooltip-*` tokens. High contrast follows the VS Code contrast
+variables, and the styles handle `forced-colors` and
+`prefers-reduced-motion`.
+
## Rules
The package is shaped for a future standalone NPM split. Keep it that way:
- No `workspace:*` runtime dependencies (no `@repo/shared`,
- `@repo/webview-shared`).
-- `react` stays a peer dependency.
+ `@repo/webview-shared`); ESLint rejects `@repo/*` imports and relative
+ cross-package imports in this package.
+- `react` stays a peer dependency; the Radix overlay primitives are the
+ only other runtime dependencies.
- New entry points go in the `exports` map; consumers never deep-import
`src/`.
+- Shared internals are reached through `package.json` subpath imports
+ (`#cx`, `#storybook`). These resolve only inside this package and ship
+ with it, so they survive a standalone NPM split.
- New components define their VS Code mappings in `tokens.css`, not
inline `--vscode-*` references.
diff --git a/packages/ui/package.json b/packages/ui/package.json
index 9e5fb4fed..6a046c191 100644
--- a/packages/ui/package.json
+++ b/packages/ui/package.json
@@ -4,6 +4,10 @@
"description": "Shared component library for VS Code webviews",
"private": true,
"type": "module",
+ "sideEffects": [
+ "**/*.css",
+ "./storybook.preview.ts"
+ ],
"exports": {
".": {
"types": "./src/index.ts",
@@ -12,10 +16,17 @@
"./codicon.css": "./src/codicon.css",
"./tokens.css": "./src/tokens.css"
},
+ "imports": {
+ "#cx": "./src/cx.ts",
+ "#storybook": "./src/storybook.ts"
+ },
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
+ "@radix-ui/react-context-menu": "catalog:",
+ "@radix-ui/react-dropdown-menu": "catalog:",
+ "@radix-ui/react-tooltip": "catalog:",
"@vscode/codicons": "catalog:"
},
"peerDependencies": {
diff --git a/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx b/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx
new file mode 100644
index 000000000..d22a6d0ee
--- /dev/null
+++ b/packages/ui/src/components/ContextMenu/ContextMenu.stories.tsx
@@ -0,0 +1,119 @@
+import { expect, screen, userEvent, waitFor, within } from "storybook/test";
+
+import { FOUR_THEME_MODES } from "#storybook";
+
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuTrigger,
+} from "./ContextMenu";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+const TARGET_STYLE: React.CSSProperties = {
+ display: "grid",
+ placeItems: "center",
+ width: 240,
+ height: 120,
+ border: "1px dashed var(--ui-description-foreground)",
+};
+
+const MenuExample = (): React.JSX.Element => (
+
+
+ Right-click here
+
+
+
+
+ Start workspace
+
+
+
+ Restart
+
+
+
+ Stop
+
+
+
+ More actions
+
+ Open logs
+ Edit settings
+
+
+
+
+);
+
+const meta: Meta = {
+ title: "UI/ContextMenu",
+ component: MenuExample,
+};
+
+export default meta;
+type Story = StoryObj;
+
+/* Right-click at the target's center; without coords the contextmenu
+ event fires at (0,0) and the menu opens detached from the target. */
+async function rightClickCenter(target: Element): Promise {
+ const rect = target.getBoundingClientRect();
+ await userEvent.pointer({
+ keys: "[MouseRight]",
+ target,
+ coords: {
+ clientX: rect.left + rect.width / 2,
+ clientY: rect.top + rect.height / 2,
+ },
+ });
+}
+
+export const Closed: Story = {};
+
+/* Chromatic crops to in-flow content, so snapshot stories reserve space
+ for the portalled menu. */
+const OVERLAY_SPACE: React.CSSProperties = { width: 620, height: 400 };
+
+/* Opens the menu and its submenu so Chromatic snapshots the open state. */
+export const Open: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ parameters: { chromatic: { modes: FOUR_THEME_MODES } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await rightClickCenter(canvas.getByText("Right-click here"));
+ const menu = await screen.findByRole("menu");
+ // Radix moves focus into the menu on open
+ await waitFor(() =>
+ expect(menu.contains(document.activeElement)).toBe(true),
+ );
+ // Keyboard avoids the submenu hover-open delay
+ await userEvent.keyboard("{End}{ArrowRight}");
+ await screen.findByRole("menuitem", { name: "Open logs" });
+ },
+};
+
+export const EscapeClosesMenu: Story = {
+ parameters: { chromatic: { disableSnapshot: true } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await rightClickCenter(canvas.getByText("Right-click here"));
+ await screen.findByRole("menu");
+ await userEvent.keyboard("{Escape}");
+ await waitFor(() =>
+ expect(screen.queryByRole("menu")).not.toBeInTheDocument(),
+ );
+ },
+};
diff --git a/packages/ui/src/components/ContextMenu/ContextMenu.tsx b/packages/ui/src/components/ContextMenu/ContextMenu.tsx
new file mode 100644
index 000000000..7885d68e3
--- /dev/null
+++ b/packages/ui/src/components/ContextMenu/ContextMenu.tsx
@@ -0,0 +1,100 @@
+import * as ContextMenuPrimitive from "@radix-ui/react-context-menu";
+
+import { cx } from "#cx";
+
+import "../menu.css";
+
+import type { ComponentPropsWithRef } from "react";
+
+export const ContextMenu = ContextMenuPrimitive.Root;
+export const ContextMenuTrigger = ContextMenuPrimitive.Trigger;
+export const ContextMenuGroup = ContextMenuPrimitive.Group;
+export const ContextMenuSub = ContextMenuPrimitive.Sub;
+
+export function ContextMenuContent({
+ className,
+ // Native menus wrap focus when arrowing past the last item
+ loop = true,
+ ...props
+}: ComponentPropsWithRef<
+ typeof ContextMenuPrimitive.Content
+>): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function ContextMenuSubContent({
+ className,
+ sideOffset = 2,
+ loop = true,
+ ...props
+}: ComponentPropsWithRef<
+ typeof ContextMenuPrimitive.SubContent
+>): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function ContextMenuItem({
+ className,
+ ...props
+}: ComponentPropsWithRef): React.JSX.Element {
+ return (
+
+ );
+}
+
+export function ContextMenuSubTrigger({
+ className,
+ children,
+ ...props
+}: ComponentPropsWithRef<
+ typeof ContextMenuPrimitive.SubTrigger
+>): React.JSX.Element {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function ContextMenuSeparator({
+ className,
+ ...props
+}: ComponentPropsWithRef<
+ typeof ContextMenuPrimitive.Separator
+>): React.JSX.Element {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/components/DropdownMenu/DropdownMenu.stories.tsx b/packages/ui/src/components/DropdownMenu/DropdownMenu.stories.tsx
new file mode 100644
index 000000000..b5de2f917
--- /dev/null
+++ b/packages/ui/src/components/DropdownMenu/DropdownMenu.stories.tsx
@@ -0,0 +1,137 @@
+import { expect, screen, userEvent, waitFor, within } from "storybook/test";
+
+import { FOUR_THEME_MODES, STORY_TRIGGER_CLASS } from "#storybook";
+
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "./DropdownMenu";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+const MenuExample = (): React.JSX.Element => (
+
+
+
+
+
+
+
+ Start workspace
+
+
+
+ Restart
+
+
+
+ Stop
+
+
+
+ More actions
+
+ Open logs
+ Edit settings
+
+
+
+
+);
+
+const meta: Meta = {
+ title: "UI/DropdownMenu",
+ component: MenuExample,
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Closed: Story = {};
+
+/* Chromatic crops to in-flow content, so snapshot stories reserve space
+ for the portalled menu. */
+const OVERLAY_SPACE: React.CSSProperties = { width: 540, height: 320 };
+
+/* Opens the menu and its submenu so Chromatic snapshots the open state. */
+export const Open: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ parameters: { chromatic: { modes: FOUR_THEME_MODES } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(
+ canvas.getByRole("button", { name: "Workspace actions" }),
+ );
+ const menu = await screen.findByRole("menu");
+ // Radix moves focus into the menu on open
+ await waitFor(() =>
+ expect(menu.contains(document.activeElement)).toBe(true),
+ );
+ // Keyboard avoids the submenu hover-open delay
+ await userEvent.keyboard("{End}{ArrowRight}");
+ await screen.findByRole("menuitem", { name: "Open logs" });
+ },
+};
+
+/* Long menus scroll. The default cap is the viewport space Radix
+ reports; the story lowers it via style. */
+export const ManyItems: Story = {
+ decorators: [
+ (Story) => (
+
+
+
+ ),
+ ],
+ render: () => (
+
+
+
+
+
+ {Array.from({ length: 40 }, (_, i) => (
+ Workspace {i + 1}
+ ))}
+
+
+ ),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ await userEvent.click(
+ canvas.getByRole("button", { name: "Workspace actions" }),
+ );
+ const menu = await screen.findByRole("menu");
+ await waitFor(() =>
+ expect(menu.scrollHeight).toBeGreaterThan(menu.clientHeight),
+ );
+ },
+};
+
+export const FocusReturnsToTrigger: Story = {
+ parameters: { chromatic: { disableSnapshot: true } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const trigger = canvas.getByRole("button", { name: "Workspace actions" });
+ await userEvent.click(trigger);
+ await screen.findByRole("menu");
+ await userEvent.keyboard("{Escape}");
+ await waitFor(() => expect(trigger).toHaveFocus());
+ await expect(screen.queryByRole("menu")).not.toBeInTheDocument();
+ },
+};
diff --git a/packages/ui/src/components/DropdownMenu/DropdownMenu.tsx b/packages/ui/src/components/DropdownMenu/DropdownMenu.tsx
new file mode 100644
index 000000000..4ebdfd449
--- /dev/null
+++ b/packages/ui/src/components/DropdownMenu/DropdownMenu.tsx
@@ -0,0 +1,106 @@
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
+
+import { cx } from "#cx";
+
+import "../menu.css";
+
+import type { ComponentPropsWithRef } from "react";
+
+export const DropdownMenu = DropdownMenuPrimitive.Root;
+export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
+export const DropdownMenuGroup = DropdownMenuPrimitive.Group;
+export const DropdownMenuSub = DropdownMenuPrimitive.Sub;
+
+export function DropdownMenuContent({
+ className,
+ sideOffset = 2,
+ align = "start",
+ // Native menus wrap focus when arrowing past the last item
+ loop = true,
+ ...props
+}: ComponentPropsWithRef<
+ typeof DropdownMenuPrimitive.Content
+>): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function DropdownMenuSubContent({
+ className,
+ sideOffset = 2,
+ loop = true,
+ ...props
+}: ComponentPropsWithRef<
+ typeof DropdownMenuPrimitive.SubContent
+>): React.JSX.Element {
+ return (
+
+
+
+ );
+}
+
+export function DropdownMenuItem({
+ className,
+ ...props
+}: ComponentPropsWithRef<
+ typeof DropdownMenuPrimitive.Item
+>): React.JSX.Element {
+ return (
+
+ );
+}
+
+export function DropdownMenuSubTrigger({
+ className,
+ children,
+ ...props
+}: ComponentPropsWithRef<
+ typeof DropdownMenuPrimitive.SubTrigger
+>): React.JSX.Element {
+ return (
+
+ {children}
+
+
+ );
+}
+
+export function DropdownMenuSeparator({
+ className,
+ ...props
+}: ComponentPropsWithRef<
+ typeof DropdownMenuPrimitive.Separator
+>): React.JSX.Element {
+ return (
+
+ );
+}
diff --git a/packages/ui/src/components/Tooltip/Tooltip.css b/packages/ui/src/components/Tooltip/Tooltip.css
new file mode 100644
index 000000000..c360f2042
--- /dev/null
+++ b/packages/ui/src/components/Tooltip/Tooltip.css
@@ -0,0 +1,36 @@
+/* Matches the native workbench hover widget; typography inherits from
+ the webview body. */
+.ui-tooltip {
+ box-sizing: border-box;
+ max-width: min(700px, calc(100vw - 16px));
+ /* Cap to the space Radix reports; oversized content scrolls */
+ max-height: var(--radix-tooltip-content-available-height);
+ overflow-y: auto;
+ padding: 4px 8px;
+ color: var(--ui-tooltip-foreground);
+ background: var(--ui-tooltip-background);
+ border: 1px solid var(--ui-tooltip-border);
+ border-radius: 5px;
+ box-shadow: var(--ui-widget-shadow);
+ overflow-wrap: break-word;
+ z-index: 40;
+ animation: ui-tooltip-fade-in 100ms linear;
+}
+
+@keyframes ui-tooltip-fade-in {
+ from {
+ opacity: 0;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .ui-tooltip {
+ animation: none;
+ }
+}
+
+@media (forced-colors: active) {
+ .ui-tooltip {
+ border-color: CanvasText;
+ }
+}
diff --git a/packages/ui/src/components/Tooltip/Tooltip.stories.tsx b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx
new file mode 100644
index 000000000..4606da88d
--- /dev/null
+++ b/packages/ui/src/components/Tooltip/Tooltip.stories.tsx
@@ -0,0 +1,117 @@
+import { expect, screen, userEvent, waitFor } from "storybook/test";
+
+import { FOUR_THEME_MODES, STORY_TRIGGER_CLASS } from "#storybook";
+
+import { Tooltip } from "./Tooltip";
+
+import type { Meta, StoryObj } from "@storybook/react-vite";
+
+const LONG_TEXT =
+ "This workspace has been running for 14 days without a rebuild. " +
+ "Stopping it frees compute resources, and any unsaved changes in " +
+ "the home volume are preserved until the next start.";
+
+const meta: Meta = {
+ title: "UI/Tooltip",
+ component: Tooltip,
+ args: {
+ content: "Stops the workspace",
+ // Instant in stories; the component default is 500ms
+ delayDuration: 0,
+ children: (
+
+ ),
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+/* findByRole("tooltip") matches Radix's visually hidden a11y copy, so
+ open assertions target the visible .ui-tooltip bubble instead. */
+async function openTooltipWithKeyboard(): Promise {
+ await userEvent.tab();
+ await screen.findByRole("tooltip");
+ const tooltip = document.querySelector(".ui-tooltip");
+ if (!tooltip) {
+ throw new Error("visible tooltip content not found");
+ }
+ await waitFor(() => expect(tooltip).toBeVisible());
+ return tooltip;
+}
+
+/* Chromatic crops to in-flow content; the tooltip opens upward, so the
+ wrapper reserves space above a bottom-anchored trigger. */
+function tooltipSpace(
+ width: number,
+ height: number | string,
+): (Story: React.ComponentType) => React.JSX.Element {
+ const space: React.CSSProperties = {
+ width,
+ height,
+ // Keeps the trigger's focus outline inside the cropped bounds
+ paddingBottom: 8,
+ display: "flex",
+ alignItems: "flex-end",
+ justifyContent: "center",
+ };
+ return function TooltipSpace(Story) {
+ return (
+
+
+
+ );
+ };
+}
+
+export const Closed: Story = {};
+
+/* Keyboard focus opens instantly and proves keyboard users get the tooltip. */
+export const Open: Story = {
+ decorators: [tooltipSpace(280, 100)],
+ parameters: { chromatic: { modes: FOUR_THEME_MODES } },
+ play: async () => {
+ await openTooltipWithKeyboard();
+ },
+};
+
+/* Long content wraps inside the native hover widget's max-width. */
+export const LongContent: Story = {
+ decorators: [tooltipSpace(720, 160)],
+ args: {
+ content: LONG_TEXT,
+ },
+ play: async () => {
+ await openTooltipWithKeyboard();
+ },
+};
+
+/* Overflowing content scrolls inside the tooltip. The default cap is the
+ viewport space Radix reports; the story lowers it via style. */
+export const OverflowScrolls: Story = {
+ decorators: [tooltipSpace(720, 240)],
+ args: {
+ content: Array.from({ length: 8 }, () => LONG_TEXT).join(" "),
+ style: { maxHeight: 160 },
+ },
+ play: async () => {
+ const tooltip = await openTooltipWithKeyboard();
+ await waitFor(() =>
+ expect(tooltip.scrollHeight).toBeGreaterThan(tooltip.clientHeight),
+ );
+ },
+};
+
+export const ClosesOnEscape: Story = {
+ parameters: { chromatic: { disableSnapshot: true } },
+ play: async () => {
+ await userEvent.tab();
+ await screen.findByRole("tooltip");
+ await userEvent.keyboard("{Escape}");
+ await waitFor(() =>
+ expect(screen.queryByRole("tooltip")).not.toBeInTheDocument(),
+ );
+ },
+};
diff --git a/packages/ui/src/components/Tooltip/Tooltip.tsx b/packages/ui/src/components/Tooltip/Tooltip.tsx
new file mode 100644
index 000000000..180b8c9e5
--- /dev/null
+++ b/packages/ui/src/components/Tooltip/Tooltip.tsx
@@ -0,0 +1,44 @@
+import * as TooltipPrimitive from "@radix-ui/react-tooltip";
+
+import { cx } from "#cx";
+
+import "./Tooltip.css";
+
+import type { ComponentPropsWithRef, ReactNode } from "react";
+
+export interface TooltipProps extends Omit<
+ ComponentPropsWithRef,
+ "content"
+> {
+ content: ReactNode;
+ /** The trigger element; must accept a forwarded ref (asChild). */
+ children: ReactNode;
+ /** Show delay in ms. Defaults to VS Code's workbench.hover.delay. */
+ delayDuration?: number;
+}
+
+export function Tooltip({
+ content,
+ children,
+ className,
+ sideOffset = 4,
+ delayDuration = 500,
+ ...props
+}: TooltipProps): React.JSX.Element {
+ return (
+
+
+ {children}
+
+
+ {content}
+
+
+
+
+ );
+}
diff --git a/packages/ui/src/components/menu.css b/packages/ui/src/components/menu.css
new file mode 100644
index 000000000..d5b2b3c8a
--- /dev/null
+++ b/packages/ui/src/components/menu.css
@@ -0,0 +1,108 @@
+/* Shared ContextMenu/DropdownMenu surface matching the native menu
+ widget; typography inherits from the webview body. */
+.ui-menu {
+ --ui-menu-gutter: 2em;
+ --ui-menu-highlight-foreground: var(--ui-menu-selection-foreground);
+ --ui-menu-highlight-background: var(--ui-menu-selection-background);
+ --ui-menu-disabled-foreground: var(--ui-disabled-foreground);
+
+ box-sizing: border-box;
+ min-width: 160px;
+ /* Cap to the space Radix reports (named per primitive) so long menus scroll */
+ max-height: var(
+ --radix-dropdown-menu-content-available-height,
+ var(--radix-context-menu-content-available-height)
+ );
+ overflow-y: auto;
+ padding: 4px;
+ color: var(--ui-menu-foreground);
+ background: var(--ui-menu-background);
+ border: 1px solid var(--ui-menu-border);
+ border-radius: var(--ui-menu-radius);
+ box-shadow: var(--ui-widget-shadow);
+ outline: 0;
+ z-index: 40;
+ animation: ui-menu-fade-in 83ms linear;
+}
+
+.ui-menu__item {
+ position: relative;
+ display: flex;
+ align-items: center;
+ height: 24px;
+ /* Native label inset; the gutters double as the check/icon columns. */
+ padding: 0 var(--ui-menu-gutter);
+ line-height: 1;
+ white-space: nowrap;
+ border-radius: var(--ui-menu-item-radius);
+ cursor: default;
+ user-select: none;
+ outline: 1px solid transparent;
+ outline-offset: -1px;
+}
+
+/* Selection border only resolves in high contrast; an open submenu
+ keeps its parent highlighted, like native. */
+.ui-menu__item[data-highlighted],
+.ui-menu__item[data-state="open"] {
+ forced-color-adjust: none;
+ color: var(--ui-menu-highlight-foreground);
+ background: var(--ui-menu-highlight-background);
+ outline-color: var(--ui-menu-selection-border);
+}
+
+.ui-menu__item[data-disabled] {
+ color: var(--ui-menu-disabled-foreground);
+}
+
+/* Leading icons sit in the left gutter so labels stay aligned */
+.ui-menu__item > .codicon:not(.ui-menu__submenu-indicator) {
+ position: absolute;
+ left: 0;
+ width: var(--ui-menu-gutter);
+ text-align: center;
+}
+
+/* Chevron in the right gutter, like the native indicator */
+.ui-menu__submenu-indicator {
+ position: absolute;
+ right: 0;
+ width: var(--ui-menu-gutter);
+ text-align: center;
+}
+
+.ui-menu__item[data-disabled] .ui-menu__submenu-indicator {
+ opacity: 0.4;
+}
+
+.ui-menu__separator {
+ height: 1px;
+ margin: 5px 0;
+ background: var(--ui-menu-separator);
+}
+
+@keyframes ui-menu-fade-in {
+ from {
+ opacity: 0;
+ }
+}
+
+@media (prefers-reduced-motion: reduce) {
+ .ui-menu {
+ animation: none;
+ }
+}
+
+@media (forced-colors: active) {
+ .ui-menu {
+ --ui-menu-highlight-foreground: HighlightText;
+ --ui-menu-highlight-background: Highlight;
+ --ui-menu-disabled-foreground: GrayText;
+
+ border-color: CanvasText;
+ }
+
+ .ui-menu__separator {
+ background: CanvasText;
+ }
+}
diff --git a/packages/ui/src/cx.ts b/packages/ui/src/cx.ts
new file mode 100644
index 000000000..e8e639d30
--- /dev/null
+++ b/packages/ui/src/cx.ts
@@ -0,0 +1,4 @@
+/** Joins the truthy class names, dropping conditional falsy entries. */
+export function cx(...classes: Array): string {
+ return classes.filter(Boolean).join(" ");
+}
diff --git a/packages/ui/src/foundations.stories.tsx b/packages/ui/src/foundations.stories.tsx
index cbd5132fb..830edc1f4 100644
--- a/packages/ui/src/foundations.stories.tsx
+++ b/packages/ui/src/foundations.stories.tsx
@@ -1,5 +1,6 @@
import { useEffect, useState } from "react";
+import { FOUR_THEME_MODES } from "./storybook";
import { useVscodeTheme } from "./useVscodeTheme";
import type { Meta, StoryObj } from "@storybook/react-vite";
@@ -132,12 +133,7 @@ const meta: Meta = {
// Snapshot every theme; tokens are the single theming surface, so this
// is where theme regressions show up.
chromatic: {
- modes: {
- light: { theme: "light" },
- dark: { theme: "dark" },
- "high-contrast": { theme: "high-contrast" },
- "high-contrast-light": { theme: "high-contrast-light" },
- },
+ modes: FOUR_THEME_MODES,
},
},
};
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index fb38844db..c1db9bf1b 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -1 +1,24 @@
+export {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuGroup,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuSub,
+ ContextMenuSubContent,
+ ContextMenuSubTrigger,
+ ContextMenuTrigger,
+} from "./components/ContextMenu/ContextMenu";
+export {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuSub,
+ DropdownMenuSubContent,
+ DropdownMenuSubTrigger,
+ DropdownMenuTrigger,
+} from "./components/DropdownMenu/DropdownMenu";
+export { Tooltip, type TooltipProps } from "./components/Tooltip/Tooltip";
export { useVscodeTheme, type VscodeThemeKind } from "./useVscodeTheme";
diff --git a/packages/ui/src/story-helpers.css b/packages/ui/src/story-helpers.css
new file mode 100644
index 000000000..a9f6edeb4
--- /dev/null
+++ b/packages/ui/src/story-helpers.css
@@ -0,0 +1,19 @@
+/* Story stand-in: a VS Code secondary button like consuming webviews render. */
+.story-trigger {
+ padding: 0.4em 1em;
+ border: 1px solid var(--vscode-button-border, transparent);
+ border-radius: 2px;
+ background: var(--vscode-button-secondaryBackground);
+ color: var(--vscode-button-secondaryForeground);
+ font: inherit;
+ cursor: pointer;
+}
+
+.story-trigger:hover {
+ background: var(--vscode-button-secondaryHoverBackground);
+}
+
+.story-trigger:focus-visible {
+ outline: 1px solid var(--vscode-focusBorder);
+ outline-offset: 2px;
+}
diff --git a/packages/ui/src/storybook.ts b/packages/ui/src/storybook.ts
new file mode 100644
index 000000000..2b2a386f0
--- /dev/null
+++ b/packages/ui/src/storybook.ts
@@ -0,0 +1,11 @@
+import "./story-helpers.css";
+
+export const FOUR_THEME_MODES = {
+ light: { theme: "light" },
+ dark: { theme: "dark" },
+ "high-contrast": { theme: "high-contrast" },
+ "high-contrast-light": { theme: "high-contrast-light" },
+} as const;
+
+/* Story stand-in for a webview-styled button. */
+export const STORY_TRIGGER_CLASS = "story-trigger";
diff --git a/packages/ui/src/tokens.css b/packages/ui/src/tokens.css
index e0cae1cbb..a1b333f75 100644
--- a/packages/ui/src/tokens.css
+++ b/packages/ui/src/tokens.css
@@ -45,6 +45,37 @@
--ui-error-foreground: var(--vscode-errorForeground);
--ui-warning-foreground: var(--vscode-editorWarning-foreground);
+ /* Disabled text */
+ --ui-disabled-foreground: var(--vscode-disabledForeground);
+
+ /* widget.shadow is null in high contrast, hiding the shadow like native */
+ --ui-widget-shadow: 0 0 12px var(--vscode-widget-shadow, transparent);
+
+ /* Menus; radius fallbacks predate the corner radius tokens */
+ --ui-menu-background: var(--vscode-menu-background);
+ --ui-menu-foreground: var(--vscode-menu-foreground);
+ --ui-menu-border: var(
+ --vscode-menu-border,
+ var(--vscode-editorWidget-border, transparent)
+ );
+ --ui-menu-selection-background: var(
+ --vscode-menu-selectionBackground,
+ transparent
+ );
+ --ui-menu-selection-foreground: var(
+ --vscode-menu-selectionForeground,
+ var(--ui-menu-foreground)
+ );
+ --ui-menu-selection-border: var(--vscode-menu-selectionBorder, transparent);
+ --ui-menu-separator: var(--vscode-menu-separatorBackground);
+ --ui-menu-radius: var(--vscode-cornerRadius-large, 5px);
+ --ui-menu-item-radius: var(--vscode-cornerRadius-medium, 4px);
+
+ /* Tooltip (native workbench hover widget) */
+ --ui-tooltip-background: var(--vscode-editorHoverWidget-background);
+ --ui-tooltip-foreground: var(--vscode-editorHoverWidget-foreground);
+ --ui-tooltip-border: var(--vscode-editorHoverWidget-border);
+
/* Only defined by high contrast themes; transparent elsewhere */
--ui-contrast-border: var(--vscode-contrastBorder, transparent);
--ui-contrast-active-border: var(--vscode-contrastActiveBorder, transparent);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 9666dc1ec..76834986c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -7,6 +7,15 @@ settings:
catalogs:
default:
+ '@radix-ui/react-context-menu':
+ specifier: ^2.3.3
+ version: 2.3.3
+ '@radix-ui/react-dropdown-menu':
+ specifier: ^2.1.20
+ version: 2.1.20
+ '@radix-ui/react-tooltip':
+ specifier: ^1.2.12
+ version: 1.2.12
'@rolldown/plugin-babel':
specifier: ^0.2.3
version: 0.2.3
@@ -438,6 +447,15 @@ importers:
packages/ui:
dependencies:
+ '@radix-ui/react-context-menu':
+ specifier: 'catalog:'
+ version: 2.3.3(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-dropdown-menu':
+ specifier: 'catalog:'
+ version: 2.1.20(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-tooltip':
+ specifier: 'catalog:'
+ version: 1.2.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
'@vscode/codicons':
specifier: 'catalog:'
version: 0.0.45
@@ -1015,6 +1033,21 @@ packages:
'@noble/hashes':
optional: true
+ '@floating-ui/core@1.8.0':
+ resolution: {integrity: sha512-0CIZ5itps/8x7BG8dEIhs53BvCUH2PCoogtakwRTut+Arm58sJooJ0AuZhLw2HJYIR5cMLNPBSS728sPho2khQ==}
+
+ '@floating-ui/dom@1.8.0':
+ resolution: {integrity: sha512-yXSrzeHZBTZadLOlfyhCkJHNeLJnHRnRInwdZ40L7ZiaAtrBwoYlsDrX3v5zB1Utk7CLfzcOVnVVWoXEky7Ceg==}
+
+ '@floating-ui/react-dom@2.1.9':
+ resolution: {integrity: sha512-JDjEFGCpImxDCA7JJKviA0M9+RtmJdj0m/NVU5IMgBK+AmZouAQQ7/+2GLH0GXXY0YMw9oXPB8hKdbPYg5QLYg==}
+ peerDependencies:
+ react: '>=16.8.0'
+ react-dom: '>=16.8.0'
+
+ '@floating-ui/utils@0.2.12':
+ resolution: {integrity: sha512-HpCo8tmWzLVad5s2d19EhAz5zqrrQ6s69qd6moPMQvkOuSwDT1YgRfWSVuc4ennqrgv3OHppiOGMQ7oC13yIww==}
+
'@humanfs/core@0.19.2':
resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==}
engines: {node: '>=18.18.0'}
@@ -1511,6 +1544,311 @@ packages:
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'}
+ '@radix-ui/primitive@1.1.5':
+ resolution: {integrity: sha512-d86WIWFYNtGA0H/d8exstrTRTp7eWJYlYJbtNofxr/3ljupZYn6EFDG/Qgu/0Kc8v7yMUxySagqJsL1+PdYjWg==}
+
+ '@radix-ui/react-arrow@1.1.11':
+ resolution: {integrity: sha512-Kdil9BB1rIFC/khmf4hC35bn8701AJcizTU7G7cUbEbk5XqqbjDuHW60uUfKqO5WojjZcbAW51Q7P0hRmMLw8A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-collection@1.1.12':
+ resolution: {integrity: sha512-nb67INpE0IahJKN7EYPp9m9YGwYeKlnzxT3MwXVkgCskaSJia97kG4T0ywpjNUSSnoJk/uvk12V8vbrEHEj+/Q==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-compose-refs@1.1.3':
+ resolution: {integrity: sha512-rYOP8OMnuuPMQF1uhPVlGNcCDlkokKqGFE3JcxFViIkAXP7EvFWUliJAstrapypaBLJNHbZL6jGhbVDGTwmVhA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-context-menu@2.3.3':
+ resolution: {integrity: sha512-PS+gKE0z2prJ74Y0sM+brAGK4mYOHIR7TlcV5EJgUQ6E0xMvyswkK2X4yRqyganrzsRL+WCSKAPu0NQITICRWg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-context@1.2.0':
+ resolution: {integrity: sha512-fOE+JtN9rygNZkCnHRBEP0TAvLldlhyOxMsbwFvTP4nAs+nBmfnna+o/Zski2wkmY1YMrFC0aSzsHoLY47iLrg==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-direction@1.1.2':
+ resolution: {integrity: sha512-C3vFhbyi4SW3PmbAi6Awpu4OzJtd0MxGurvSsYtr7p7nM8RNB3VAF3CUmnp2j50knpkrRcB7+ycVXzgLgF6yNA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-dismissable-layer@1.1.15':
+ resolution: {integrity: sha512-b0XaRlzn2QKuo10XyNgi2DAJDf5XC9d1nD3FJcuvCjbR7+4Ad28zmZsLsqx+hvDEzMnRuZaZxZm9gYObV6RmRA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-dropdown-menu@2.1.20':
+ resolution: {integrity: sha512-slfm+rRaZRuQBvHq60lXvSVUPhid0IPtjSZzIuUlWZMUs01iYZNlGS3mJgRD3ChLQVBAYlKiL/tFyWGX+dz8Xw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-focus-guards@1.1.4':
+ resolution: {integrity: sha512-cot/aB/mOm0IYVYTTmQcEEK1M48lZWi8FlYe5nDPQQ8NYZUlXEFgncJ9p2Kzer3RKSrY7cTTpEMLZKNo9QoP5Q==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-focus-scope@1.1.12':
+ resolution: {integrity: sha512-jjk/lqTeNL0azUx5ZYzVrl4NgaDIrdzTNE4mABV9yBFI7FQqN7pIgzV1bTleUezP2QiTGA1BFTqY8MegDgWX9A==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-id@1.1.2':
+ resolution: {integrity: sha512-orBC88futVpqCmhX1p4cvquNHsELQ+w+vBJnuj3ftETI5bJb0bZn3Tqu3SWN2IOcPycTnMGnhwoermvISt72sA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-menu@2.1.20':
+ resolution: {integrity: sha512-VsUrXxFe9d2ScbZF0fR/oPR1+qjyeLs5p0jzG8h90puMoA9bq4SirYlXbE+USRg9Q2qTeJSFNqjw2nts8jJe4w==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-popper@1.3.3':
+ resolution: {integrity: sha512-mS7dGpyjv6b+gsDjLF7e0ia1W4Im1B1hSCy2yuXlHuvnZxHKagfDaobt/KAKt27EpZMit2pss8eJBVyVjEWM+g==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-portal@1.1.13':
+ resolution: {integrity: sha512-z3oXfmaHLJTF1wktbjgD6cn9jiEbq3WSondB10LIuIt2m2Ym4iJlrW04/euMwENDdWDdE7z+OuY7Qyp1YpRSwA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-presence@1.1.7':
+ resolution: {integrity: sha512-zBZ4QM5XG3JRanDmqXYf3MD6th4AFXFmgU6KNMFzUaV6F3uw9I5/zjMUvFriSEn5ewo1nxuibvyxJdmLlDcslA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-primitive@2.1.7':
+ resolution: {integrity: sha512-bC3NiwsprbxKjuon9l7X6BUTw7FPVzEYaL92MPEY5SCd/9hUTPXVFtVwRix7778wtRsVao+zE062gL79FZleeQ==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-roving-focus@1.1.15':
+ resolution: {integrity: sha512-40svmmugfM3mUN7VUDGVE1tQGOhyi8enlGD0CNJEcMM36C1f71PKM21DFgNHUfem0XnA+d8H8oN3Z9ZpJjSslg==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-slot@1.3.0':
+ resolution: {integrity: sha512-MojKku4U/miO8Av4Dkb+ctMAQx7JmY96LmtDQlAarCRtd7rN52QCSzBF+XAvr5S6coSVj9HEPBgHAHKEJVk/WA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-tooltip@1.2.12':
+ resolution: {integrity: sha512-U3HoftgWnmla78vzQbLvKKb7bUYJxoiiqYFzp1wu/TBMyDqMZSuCl3aRICsD6EfVEwcJD2mumGDGUXLFVqQHKA==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/react-use-callback-ref@1.1.2':
+ resolution: {integrity: sha512-xCso9j1/u8sEgP1RNHjFrXJLApL8LiqOkI1R4ywuN00rxWdYg4oQXuwKLS3i0j5NWLromUD27/4nlxj2UFVvIw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-controllable-state@1.2.3':
+ resolution: {integrity: sha512-PLzC90MS+ReootmjC597dvopoelpZ8Q61HJkDXZSExitIq7PL55vHNnesAHwguHK0aPfBnpdNzQtv1uliaqQrA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-effect-event@0.0.3':
+ resolution: {integrity: sha512-6c8ZqvPTWILEKnyVkP53EGRCcpnJiKTC21sS/6R1GF5xKyHJJWQEPfkqlcgUkdRQivd6tb23abUwe4ngWmY0JA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-is-hydrated@0.1.1':
+ resolution: {integrity: sha512-qwOiz4Tjo8CNnrOLAYUMXeZwDzXgXpvK4TKQPmWLECM9XoWvA6+0Z2/7Ag3A4ivjS4ovbLJPbskkxioFyBhr8A==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-layout-effect@1.1.2':
+ resolution: {integrity: sha512-jrBWOxZITuGcnjRCM2t2U5ZPkCLxD+Ym6DjfssS5haTj2iiak/DOb64JeN6OdLfLgptb6/e2kKR+ZuTrGoZTPA==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-rect@1.1.2':
+ resolution: {integrity: sha512-d8a+bBY/FxikNPlgJJoaBHZX+zKVbWHYJGTLnLvveQgFSTntkGdEKv3JDtHrMS0DNYpllz2nRsTLGLKYttbpmw==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-use-size@1.1.2':
+ resolution: {integrity: sha512-giWQp+4mxjBPt4KZ0MmyuykFNWfbDxKt4x+fPkRYmgRFJSbCZFzUglvMb/Kjn38tm10YP4ufiQZDx3zna4LU6w==}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ '@radix-ui/react-visually-hidden@1.2.7':
+ resolution: {integrity: sha512-1wNZBggTDK3GRuuQ6nP4k2yi7a6l7I5qbMPbZcRsrGsGVead/f/d5FhEzUvqFs0bcrDLx7n1zKQ3JvLR6whaaw==}
+ peerDependencies:
+ '@types/react': '*'
+ '@types/react-dom': '*'
+ react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+ '@types/react-dom':
+ optional: true
+
+ '@radix-ui/rect@1.1.2':
+ resolution: {integrity: sha512-xnXE7wG13PI+cxieVssYXlQJuYVRhH9NBoxt3KNwzghDIA69GMm7d4wXRouHIYjE+KvS6U/MsMO73NdS2MH9ZA==}
+
'@rolldown/binding-android-arm64@1.1.4':
resolution: {integrity: sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==}
engines: {node: ^20.19.0 || >=22.12.0}
@@ -2340,6 +2678,10 @@ packages:
argparse@2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+ aria-hidden@1.2.6:
+ resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+ engines: {node: '>=10'}
+
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
@@ -2748,6 +3090,9 @@ packages:
resolution: {integrity: sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
+ detect-node-es@1.1.0:
+ resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
detect-node@2.1.0:
resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==}
@@ -3208,6 +3553,10 @@ packages:
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
engines: {node: '>= 0.4'}
+ get-nonce@1.0.1:
+ resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+ engines: {node: '>=6'}
+
get-proto@1.0.1:
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
engines: {node: '>= 0.4'}
@@ -4298,6 +4647,36 @@ packages:
react-is@17.0.2:
resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==}
+ react-remove-scroll-bar@2.3.8:
+ resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-remove-scroll@2.7.2:
+ resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ react-style-singleton@2.2.3:
+ resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
react@19.2.7:
resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==}
engines: {node: '>=0.10.0'}
@@ -4863,6 +5242,26 @@ packages:
url-join@4.0.1:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
+ use-callback-ref@1.3.3:
+ resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
+ use-sidecar@1.1.3:
+ resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+ engines: {node: '>=10'}
+ peerDependencies:
+ '@types/react': '*'
+ react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+ peerDependenciesMeta:
+ '@types/react':
+ optional: true
+
use-sync-external-store@1.6.0:
resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==}
peerDependencies:
@@ -5641,6 +6040,23 @@ snapshots:
'@exodus/bytes@1.15.1': {}
+ '@floating-ui/core@1.8.0':
+ dependencies:
+ '@floating-ui/utils': 0.2.12
+
+ '@floating-ui/dom@1.8.0':
+ dependencies:
+ '@floating-ui/core': 1.8.0
+ '@floating-ui/utils': 0.2.12
+
+ '@floating-ui/react-dom@2.1.9(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@floating-ui/dom': 1.8.0
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+
+ '@floating-ui/utils@0.2.12': {}
+
'@humanfs/core@0.19.2':
dependencies:
'@humanfs/types': 0.15.0
@@ -6106,6 +6522,288 @@ snapshots:
'@pkgjs/parseargs@0.11.0':
optional: true
+ '@radix-ui/primitive@1.1.5': {}
+
+ '@radix-ui/react-arrow@1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-collection@1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-compose-refs@1.1.3(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-context-menu@2.3.3(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-context@1.2.0(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-direction@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-dismissable-layer@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-dropdown-menu@2.1.20(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-menu': 2.1.20(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-focus-guards@1.1.4(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-focus-scope@1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-id@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-menu@2.1.20(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-focus-guards': 1.1.4(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-focus-scope': 1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-roving-focus': 1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ aria-hidden: 1.2.6
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ react-remove-scroll: 2.7.2(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-popper@1.3.3(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@floating-ui/react-dom': 2.1.9(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-arrow': 1.1.11(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-rect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-size': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/rect': 1.1.2
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-portal@1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-presence@1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-primitive@2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-roving-focus@1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-collection': 1.1.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-direction': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-use-callback-ref': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-is-hydrated': 0.1.1(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-slot@1.3.0(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-tooltip@1.2.12(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/primitive': 1.1.5
+ '@radix-ui/react-compose-refs': 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-context': 1.2.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-dismissable-layer': 1.1.15(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-id': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-popper': 1.3.3(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-portal': 1.1.13(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-presence': 1.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ '@radix-ui/react-slot': 1.3.0(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-controllable-state': 1.2.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-visually-hidden': 1.2.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/react-use-callback-ref@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-controllable-state@1.2.3(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-use-effect-event': 0.0.3(@types/react@19.2.17)(react@19.2.7)
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-effect-event@0.0.3(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-is-hydrated@0.1.1(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-layout-effect@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-rect@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/rect': 1.1.2
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-use-size@1.1.2(@types/react@19.2.17)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-use-layout-effect': 1.1.2(@types/react@19.2.17)(react@19.2.7)
+ react: 19.2.7
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ '@radix-ui/react-visually-hidden@1.2.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)':
+ dependencies:
+ '@radix-ui/react-primitive': 2.1.7(@types/react-dom@19.2.3)(@types/react@19.2.17)(react-dom@19.2.7)(react@19.2.7)
+ react: 19.2.7
+ react-dom: 19.2.7(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+ '@types/react-dom': 19.2.3(@types/react@19.2.17)
+
+ '@radix-ui/rect@1.1.2': {}
+
'@rolldown/binding-android-arm64@1.1.4':
optional: true
@@ -6977,6 +7675,10 @@ snapshots:
argparse@2.0.1: {}
+ aria-hidden@1.2.6:
+ dependencies:
+ tslib: 2.8.1
+
aria-query@5.3.0:
dependencies:
dequal: 2.0.3
@@ -7375,6 +8077,8 @@ snapshots:
detect-newline@4.0.1: {}
+ detect-node-es@1.1.0: {}
+
detect-node@2.1.0:
optional: true
@@ -7919,6 +8623,8 @@ snapshots:
hasown: 2.0.4
math-intrinsics: 1.1.0
+ get-nonce@1.0.1: {}
+
get-proto@1.0.1:
dependencies:
dunder-proto: 1.0.1
@@ -9292,6 +9998,33 @@ snapshots:
react-is@17.0.2: {}
+ react-remove-scroll-bar@2.3.8(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7)
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ react-remove-scroll@2.7.2(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ react-remove-scroll-bar: 2.3.8(@types/react@19.2.17)(react@19.2.7)
+ react-style-singleton: 2.2.3(@types/react@19.2.17)(react@19.2.7)
+ tslib: 2.8.1
+ use-callback-ref: 1.3.3(@types/react@19.2.17)(react@19.2.7)
+ use-sidecar: 1.1.3(@types/react@19.2.17)(react@19.2.7)
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ react-style-singleton@2.2.3(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ get-nonce: 1.0.1
+ react: 19.2.7
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.17
+
react@19.2.7: {}
read-pkg@9.0.1:
@@ -9912,6 +10645,21 @@ snapshots:
url-join@4.0.1: {}
+ use-callback-ref@1.3.3(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ react: 19.2.7
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.17
+
+ use-sidecar@1.1.3(@types/react@19.2.17)(react@19.2.7):
+ dependencies:
+ detect-node-es: 1.1.0
+ react: 19.2.7
+ tslib: 2.8.1
+ optionalDependencies:
+ '@types/react': 19.2.17
+
use-sync-external-store@1.6.0(react@19.2.7):
dependencies:
react: 19.2.7
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index fd1f735b9..db80f00c4 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -2,6 +2,9 @@ packages:
- packages/*
catalog:
+ "@radix-ui/react-context-menu": ^2.3.3
+ "@radix-ui/react-dropdown-menu": ^2.1.20
+ "@radix-ui/react-tooltip": ^1.2.12
"@rolldown/plugin-babel": ^0.2.3
"@storybook/addon-a11y": ^10.4.6
"@storybook/addon-docs": ^10.4.6