Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion apps/web/content/docs/api/shadcn.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Your app must have Tailwind CSS configured.
<tbody>
<tr>
<td><code>@json-render/shadcn</code></td>
<td><code>shadcnComponents</code></td>
<td><code>shadcnComponents</code>, <code>PortalContainerProvider</code></td>
<td>React implementations</td>
</tr>
<tr>
Expand Down Expand Up @@ -73,6 +73,21 @@ const { registry } = defineRegistry(catalog, {

State actions (`setState`, `pushState`, `removeState`) are built into the React schema and handled by `ActionProvider` automatically. You don't need to declare them in your catalog.

## Portal containers and scoped themes

Dialog, DropdownMenu, Popover, Select and Tooltip normally portal to `document.body`. When the renderer lives inside a scoped theme, supply a stable host element with that theme through `PortalContainerProvider`:

```tsx
import { PortalContainerProvider } from "@json-render/shadcn";
import { Renderer } from "@json-render/react";

<PortalContainerProvider container={themedPortalElement}>
<Renderer spec={spec} registry={registry} />
</PortalContainerProvider>
```

The host owns the element and its lifetime. Place it outside clipped or transformed ancestors when overlays must cover the viewport. Nested providers select their nearest container. Without a provider, or with `container={undefined}`, the standard Radix body portal remains unchanged. The provider does not change component schemas, state, actions or overlay interactions.

## Extending with Custom Components

Add custom components alongside standard ones:
Expand Down
17 changes: 16 additions & 1 deletion packages/shadcn/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,21 @@ function App({ spec }) {
}
```

## Portal containers and scoped themes

Dialog, DropdownMenu, Popover, Select and Tooltip normally portal to `document.body`. When the renderer lives inside a scoped theme, supply a stable host element with that theme through `PortalContainerProvider`:

```tsx
import { PortalContainerProvider } from "@json-render/shadcn";
import { Renderer } from "@json-render/react";

<PortalContainerProvider container={themedPortalElement}>
<Renderer spec={spec} registry={registry} />
</PortalContainerProvider>
```

The host owns the element and its lifetime. Place it outside clipped or transformed ancestors when overlays must cover the viewport. Nested providers select their nearest container. Without a provider, or with `container={undefined}`, the standard Radix body portal remains unchanged. The provider does not change component schemas, state, actions or overlay interactions.

## Extending with Custom Components

Pick standard components as a base and add your own alongside them:
Expand Down Expand Up @@ -203,7 +218,7 @@ All form components support the `validateOn` prop to control when validation run

| Entry Point | Exports |
|-------------|---------|
| `@json-render/shadcn` | `shadcnComponents` |
| `@json-render/shadcn` | `shadcnComponents`, `PortalContainerProvider` |
| `@json-render/shadcn/catalog` | `shadcnComponentDefinitions` |

The `/catalog` entry point contains only Zod schemas (no React dependency), so it can be used in server-side code for prompt generation.
2 changes: 2 additions & 0 deletions packages/shadcn/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ export {
type ComponentDefinition,
type ShadcnProps,
} from "./catalog";

export { PortalContainerProvider } from "./portal-container";
24 changes: 24 additions & 0 deletions packages/shadcn/src/portal-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"use client";

import { createContext, useContext, type ReactNode } from "react";

const PortalContainerContext = createContext<HTMLElement | undefined>(undefined);

/** Keep shadcn portals inside the host's theme and stylesheet boundary. */
export function PortalContainerProvider({
container,
children,
}: {
container: HTMLElement | undefined;
children: ReactNode;
}) {
return (
<PortalContainerContext.Provider value={container}>
{children}
</PortalContainerContext.Provider>
);
}

export function usePortalContainer() {
return useContext(PortalContainerContext);
}
4 changes: 3 additions & 1 deletion packages/shadcn/src/ui/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { XIcon } from "lucide-react";
import { Dialog as DialogPrimitive } from "radix-ui";

import { cn } from "../lib/utils";
import { usePortalContainer } from "../portal-container";
import { Button } from "./button";

function Dialog({
Expand All @@ -22,7 +23,8 @@ function DialogTrigger({
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
const container = usePortalContainer();
return <DialogPrimitive.Portal container={container} data-slot="dialog-portal" {...props} />;
}

function DialogClose({
Expand Down
7 changes: 5 additions & 2 deletions packages/shadcn/src/ui/dropdown-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react";
import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";

import { cn } from "../lib/utils";
import { usePortalContainer } from "../portal-container";

function DropdownMenu({
...props
Expand All @@ -15,8 +16,9 @@ function DropdownMenu({
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
const container = usePortalContainer();
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
<DropdownMenuPrimitive.Portal container={container} data-slot="dropdown-menu-portal" {...props} />
);
}

Expand All @@ -36,8 +38,9 @@ function DropdownMenuContent({
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
const container = usePortalContainer();
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Portal container={container}>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
Expand Down
4 changes: 3 additions & 1 deletion packages/shadcn/src/ui/popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react";
import { Popover as PopoverPrimitive } from "radix-ui";

import { cn } from "../lib/utils";
import { usePortalContainer } from "../portal-container";

function Popover({
...props
Expand All @@ -23,8 +24,9 @@ function PopoverContent({
sideOffset = 4,
...props
}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
const container = usePortalContainer();
return (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Portal container={container}>
<PopoverPrimitive.Content
data-slot="popover-content"
align={align}
Expand Down
4 changes: 3 additions & 1 deletion packages/shadcn/src/ui/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import { Select as SelectPrimitive } from "radix-ui";

import { cn } from "../lib/utils";
import { usePortalContainer } from "../portal-container";

function Select({
...props
Expand Down Expand Up @@ -55,8 +56,9 @@ function SelectContent({
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
const container = usePortalContainer();
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Portal container={container}>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
Expand Down
4 changes: 3 additions & 1 deletion packages/shadcn/src/ui/tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as React from "react";
import { Tooltip as TooltipPrimitive } from "radix-ui";

import { cn } from "../lib/utils";
import { usePortalContainer } from "../portal-container";

function TooltipProvider({
delayDuration = 0,
Expand Down Expand Up @@ -36,8 +37,9 @@ function TooltipContent({
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
const container = usePortalContainer();
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Portal container={container}>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
Expand Down
4 changes: 4 additions & 0 deletions skills/shadcn/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,7 @@ All form components support `validateOn` to control when validation runs:
- Component implementations use bundled shadcn/ui primitives (not your app's `components/ui/`)
- All form inputs support `checks` for validation (type + message pairs) and `validateOn` for timing
- Events: inputs emit `change`/`submit`/`focus`/`blur`; buttons emit `press`; selects emit `change`/`select`

## Scoped portal containers

When a renderer uses a local theme, wrap it in `PortalContainerProvider` from `@json-render/shadcn` and pass a stable themed host element through `container`. Dialog, DropdownMenu, Popover, Select and Tooltip use the nearest provider. The host owns placement and cleanup; without a provider, portals continue to use `document.body`. This is host integration, not a model-facing prop.