Compare commits

..
Author SHA1 Message Date
github-actions[bot] f67a52c55a chore: version packages 2026-03-27 23:20:22 +00:00
96 changed files with 848 additions and 3766 deletions
+13
View File
@@ -0,0 +1,13 @@
# Changesets
Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in the repository](https://github.com/changesets/changesets).
## Adding a changeset
To add a changeset, run `pnpm changeset` in the root of the repository. This will prompt you to select
which packages have changed and what type of version bump (major, minor, or patch) should be applied.
All `@json-render/*` packages are versioned together -- a changeset for any one of them will bump all
packages to the same version.
+39
View File
@@ -0,0 +1,39 @@
{
"$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
"@json-render/core",
"@json-render/react",
"@json-render/react-email",
"@json-render/react-pdf",
"@json-render/shadcn",
"@json-render/react-native",
"@json-render/remotion",
"@json-render/codegen",
"@json-render/zustand",
"@json-render/redux",
"@json-render/jotai",
"@json-render/vue",
"@json-render/xstate",
"@json-render/image",
"@json-render/mcp",
"@json-render/svelte",
"@json-render/solid",
"@json-render/react-three-fiber",
"@json-render/yaml",
"@json-render/shadcn-svelte",
"@json-render/ink",
"@json-render/next"
]
],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"privatePackages": {
"version": true,
"tag": false
}
}
-12
View File
@@ -13,18 +13,6 @@ concurrency:
cancel-in-progress: true
jobs:
version-sync:
name: Version Sync Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Check version sync
run: node scripts/check-version-sync.js
lint:
name: Lint
runs-on: ubuntu-latest
+17 -127
View File
@@ -8,62 +8,19 @@ on:
concurrency: ${{ github.workflow }}-${{ github.ref }}
permissions:
contents: write
pull-requests: write
jobs:
check-release:
name: Check for new version
release:
name: Release
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
outputs:
should_release: ${{ steps.check.outputs.should_release }}
needs_github_release: ${{ steps.check.outputs.needs_github_release }}
version: ${{ steps.check.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Compare package.json version to npm and check GitHub release
id: check
run: |
LOCAL_VERSION=$(node -p "require('./packages/core/package.json').version")
echo "Local version: $LOCAL_VERSION"
NPM_VERSION=$(npm view @json-render/core version 2>/dev/null || echo "0.0.0")
echo "npm version: $NPM_VERSION"
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
else
echo "Version unchanged on npm, skipping build and publish"
echo "should_release=false" >> "$GITHUB_OUTPUT"
TAG="v$LOCAL_VERSION"
if gh release view "$TAG" &>/dev/null; then
echo "GitHub release $TAG exists"
echo "needs_github_release=false" >> "$GITHUB_OUTPUT"
else
echo "GitHub release $TAG is missing, will create it"
echo "needs_github_release=true" >> "$GITHUB_OUTPUT"
fi
fi
echo "version=$LOCAL_VERSION" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build:
name: Build
needs: check-release
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -71,87 +28,20 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build
publish:
name: Publish to npm
needs: [check-release, build]
if: needs.check-release.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
node-version: 20
cache: pnpm
registry-url: "https://registry.npmjs.org"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build packages
run: pnpm run build
- name: Publish all public packages
run: pnpm -r publish --no-git-checks --filter '@json-render/*'
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
github-release:
name: Create GitHub Release
needs: [check-release, publish]
if: >-
always()
&& needs.check-release.outputs.needs_github_release == 'true'
&& (needs.publish.result == 'success' || needs.publish.result == 'skipped')
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Extract changelog entry
run: |
VERSION="${{ needs.check-release.outputs.version }}"
awk '/<!-- release:start -->/{found=1; next} /<!-- release:end -->/{found=0} found{print}' CHANGELOG.md > /tmp/release-notes.md
LINES=$(wc -l < /tmp/release-notes.md | tr -d ' ')
if [ "$LINES" -lt 2 ]; then
echo "Error: No release notes found between <!-- release:start --> and <!-- release:end --> markers in CHANGELOG.md"
exit 1
fi
echo "Extracted release notes for $VERSION ($LINES lines)"
- name: Create GitHub Release
run: |
VERSION="${{ needs.check-release.outputs.version }}"
TAG="v$VERSION"
if gh release view "$TAG" &>/dev/null; then
echo "Release $TAG already exists"
else
echo "Creating release $TAG..."
gh release create "$TAG" \
--title "$TAG" \
--notes-file /tmp/release-notes.md
fi
- name: Create Release Pull Request or Publish
uses: changesets/action@v1
with:
version: pnpm ci:version
publish: pnpm ci:publish
title: "chore: version packages"
commit: "chore: version packages"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
-2
View File
@@ -29,8 +29,6 @@ build
dist
*.tsbuildinfo
.svelte-kit/
tsup.config.bundled_*.mjs
next-env.d.ts
# Debug
+17 -15
View File
@@ -73,20 +73,25 @@ Do **not** add `--port` flags -- portless handles port assignment automatically.
- Skills in `skills/*/SKILL.md` (if the package has a corresponding skill)
- `AGENTS.md` (if workflow or conventions change)
## Releasing
## Releases
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
All public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.
### Fixed version group
All public `@json-render/*` packages are in a **fixed** group (see `.changeset/config.json`). A changeset that bumps any one of them bumps all of them to the same version. You only need to list the packages that actually changed in the changeset front matter — the fixed group handles the rest.
### Preparing a release
When asked to prepare a release (e.g. "prepare v0.17.0"):
When asked to prepare a release (e.g. "prepare v0.12.0"):
1. Create a branch (e.g. `prepare-v0.17.0`)
2. Bump the version in `packages/core/package.json`
3. Run `pnpm run version:sync` to update all other `@json-render/*` packages
4. Write the changelog entry in `CHANGELOG.md`, wrapped in `<!-- release:start -->` and `<!-- release:end -->` markers (move the markers from the previous entry to the new one)
1. **Create a changeset file** at `.changeset/v0-<N>-release.md` following the existing pattern:
- YAML front matter listing changed packages with bump type (`minor` for feature releases, `patch` for bug-fix-only releases)
- A one-line summary, then `### New:` / `### Improved:` / `### Fixed:` sections describing each change
- Always list `@json-render/core` plus any packages with actual code changes
2. **Do NOT bump versions** in `package.json` files — CI runs `pnpm ci:version` (which calls `changeset version`) to do that automatically
3. **Do NOT manually write `CHANGELOG.md`** entries — `changeset version` generates them from the changeset file
4. **Add new packages to the fixed group** in `.changeset/config.json` if they should be versioned together with the rest
5. **Fill documentation gaps** — every public package should have:
- A row in the root `README.md` packages table
- A renderer section in the root `README.md` (if it's a renderer)
@@ -96,15 +101,12 @@ When asked to prepare a release (e.g. "prepare v0.17.0"):
- A skill at `skills/<name>/SKILL.md`
- A `packages/<name>/README.md`
6. **Run `pnpm type-check`** after all changes to verify nothing is broken
7. Open a PR and merge to `main`
CI compares the `@json-render/core` version to what's on npm. If it differs, it builds, publishes all public packages, and creates the GitHub release automatically. The release body is extracted from the content between the markers.
### CI scripts
### Scripts
- `pnpm run version:sync` — sync all `@json-render/*` package versions to match `@json-render/core`
- `pnpm run version:check` — verify all versions are in sync (runs in CI)
- `pnpm run ci:publish` — build all packages and publish to npm (CI only)
- `pnpm changeset` — interactively create a new changeset
- `pnpm ci:version` — run `changeset version` + lockfile update (CI only)
- `pnpm ci:publish` — build all packages and publish to npm (CI only)
<!-- opensrc:start -->
-26
View File
@@ -1,26 +0,0 @@
# Changelog
## 0.17.0
<!-- release:start -->
### New Features
- **Gaussian Splatting** — Added `GaussianSplat` component to `@json-render/react-three-fiber`, bringing the component count to 20. Composable with all existing R3F components (lights, controls, post-processing) via drei's Splat loader (#259)
- **Standalone gsplat example** — Experimental demo app showcasing Gaussian Splatting with gsplat.js (no Three.js dependency), featuring scene selector, live JSON spec viewer, and progress indicator (#259)
- **R3F gsplat example** — Demo app with five scenes: splat showroom, splat with primitives, multi-splat, post-processing effects, and animated floating splat (#259)
### Improved
- **AI output quality** — Improved prompt output and schema generation for more reliable AI-generated specs (#268)
### Contributors
- @ctate
- @willmanzoli
<!-- release:end -->
## 0.16.0
### Improved
- **Release process** — Switched from Changesets to a manual single-PR release workflow with changelog markers and automatic npm publish on version bump
+2 -6
View File
@@ -27,7 +27,7 @@ npm install @json-render/core @json-render/solid
npm install @json-render/core @json-render/ink ink react
# or for full Next.js apps (routes, layouts, SSR, metadata)
npm install @json-render/core @json-render/react @json-render/next
# or for 3D scenes (and gaussian splatting via the GaussianSplat component)
# or for 3D scenes
npm install @json-render/core @json-render/react-three-fiber @react-three/fiber @react-three/drei three
```
@@ -128,7 +128,7 @@ function Dashboard({ spec }) {
| `@json-render/solid` | SolidJS renderer with fine-grained reactive contexts |
| `@json-render/shadcn` | 36 pre-built shadcn/ui components (Radix UI + Tailwind CSS) |
| `@json-render/shadcn-svelte`| 36 pre-built shadcn-svelte components (Svelte 5 + Tailwind CSS) |
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (20 built-in components, including GaussianSplat) |
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (19 built-in components) |
| `@json-render/react-native` | React Native renderer with standard mobile components |
| `@json-render/next` | Next.js renderer — JSON becomes full apps with routes, layouts, SSR |
| `@json-render/remotion` | Remotion video renderer, timeline schema |
@@ -462,7 +462,6 @@ const catalog = defineCatalog(schema, {
Sphere: threeComponentDefinitions.Sphere,
AmbientLight: threeComponentDefinitions.AmbientLight,
DirectionalLight: threeComponentDefinitions.DirectionalLight,
GaussianSplat: threeComponentDefinitions.GaussianSplat,
OrbitControls: threeComponentDefinitions.OrbitControls,
},
actions: {},
@@ -474,7 +473,6 @@ const { registry } = defineRegistry(catalog, {
Sphere: threeComponents.Sphere,
AmbientLight: threeComponents.AmbientLight,
DirectionalLight: threeComponents.DirectionalLight,
GaussianSplat: threeComponents.GaussianSplat,
OrbitControls: threeComponents.OrbitControls,
},
});
@@ -732,8 +730,6 @@ pnpm dev
- Vue Example: run `pnpm dev` in `examples/vue`
- Vite Renderers (React + Vue + Svelte + Solid): run `pnpm dev` in `examples/vite-renderers`
- React Native example: run `npx expo start` in `examples/react-native`
- Gaussian Splatting (R3F): run `pnpm dev` in `examples/react-three-fiber-gsplat`
- Gaussian Splatting (experimental standalone gsplat.js demo): run `pnpm dev` in `examples/gsplat`
## How It Works
@@ -3,7 +3,7 @@ export const metadata = pageMetadata("docs/api/react-three-fiber")
# @json-render/react-three-fiber
React Three Fiber renderer for json-render. 20 built-in 3D components for meshes, lights, models, gaussian splats, environments, text, cameras, and controls.
React Three Fiber renderer for json-render. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.
## Installation
@@ -283,11 +283,6 @@ All primitives share: <code>position</code>, <code>rotation</code>, <code>scale<
<td>Camera controls</td>
<td><code>enableDamping</code>, <code>enableZoom</code>, <code>autoRotate</code></td>
</tr>
<tr>
<td><code>GaussianSplat</code></td>
<td>Gaussian splat (.splat/.ply) loader</td>
<td><code>src</code>, <code>position</code>, <code>rotation</code>, <code>scale</code>, <code>alphaHash</code>, <code>toneMapped</code></td>
</tr>
</tbody>
</table>
+2 -5
View File
@@ -1,16 +1,13 @@
import { Header } from "@/components/header";
import { getStarCount } from "@/lib/github";
export default async function MainLayout({
export default function MainLayout({
children,
}: {
children: React.ReactNode;
}) {
const stars = await getStarCount();
return (
<div className="min-h-screen flex flex-col">
<Header stars={stars} />
<Header />
<main className="flex-1">{children}</main>
</div>
);
+1 -4
View File
@@ -17,13 +17,10 @@ const PLAYGROUND_RULES = [
"NEVER use viewport height classes (min-h-screen, h-screen) - the UI renders inside a fixed-size container.",
"NEVER use page background colors (bg-gray-50) - the container has its own background.",
"For forms or small UIs: use Card as root with maxWidth:'sm' or 'md' and centered:true.",
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts. Keep the total UI compact — avoid sprawling multi-section pages. Prefer a single focused Card over a full page layout.",
"For content-heavy UIs (blogs, dashboards, product listings): use Stack or Grid as root. Use Grid with 2-3 columns for card layouts.",
"Wrap each repeated item in a Card for visual separation and structure.",
"Use realistic, professional sample data. Include 3-5 items with varied content. Never leave state arrays empty.",
'For form inputs (Input, Textarea, Select), always include checks for validation (e.g. required, email, minLength). Always pair checks with a $bindState expression on the value prop (e.g. { "$bindState": "/path" }).',
"NEVER use emoji characters. Use the Icon component with Lucide icon names instead. For example, use Icon with name:'MapPin' instead of a pin emoji, Icon with name:'Mail' instead of an envelope emoji, etc.",
"For icon+label patterns, use a horizontal Stack with gap:'sm' and align:'center' containing an Icon and a Text.",
"For any tabular or list data with consistent columns (items, orders, stats), ALWAYS use the Table component. Never simulate tables with Stacks — the columns won't align.",
];
const MAX_PROMPT_LENGTH = 500;
+67 -66
View File
@@ -18,62 +18,65 @@ import { PlaygroundRenderer } from "@/lib/render/renderer";
import { playgroundCatalog } from "@/lib/render/catalog";
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
const SIMULATION_PROMPT = "Show a team performance dashboard";
const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
interface SimulationStage {
tree: Spec;
stream: string;
}
const DASH_STATE = {
chartData: [
{ label: "Mon", value: 12 },
{ label: "Tue", value: 28 },
{ label: "Wed", value: 19 },
{ label: "Thu", value: 34 },
{ label: "Fri", value: 45 },
{ label: "Sat", value: 38 },
{ label: "Sun", value: 52 },
],
};
// Shared state & element definitions for the progressive simulation stages.
const FORM_STATE = { form: { name: "", email: "", message: "" } };
const METRIC_REVENUE = {
type: "Metric",
const NAME_INPUT = {
type: "Input",
props: {
label: "Weekly Revenue",
value: "12,400",
prefix: "$",
change: "+18%",
changeType: "positive",
label: "Name",
name: "name",
statePath: "/form/name",
checks: [{ type: "required", message: "Name is required" }],
},
} as const;
const CHART = {
type: "LineGraph",
props: { data: { $state: "/chartData" } },
const EMAIL_INPUT = {
type: "Input",
props: {
label: "Email",
name: "email",
type: "email",
statePath: "/form/email",
checks: [
{ type: "required", message: "Email is required" },
{ type: "email", message: "Please enter a valid email" },
],
},
} as const;
const SEP = { type: "Separator", props: {} } as const;
const PROGRESS_DEALS = {
type: "Progress",
props: { value: 72, label: "Deals Closed -- 72%" },
const MESSAGE_INPUT = {
type: "Textarea",
props: {
label: "Message",
name: "message",
statePath: "/form/message",
checks: [{ type: "required", message: "Message is required" }],
},
} as const;
const PROGRESS_RETENTION = {
type: "Progress",
props: { value: 91, label: "Retention -- 91%" },
const SUBMIT_BUTTON = {
type: "Button",
props: { label: "Send Message", variant: "primary" },
on: { press: { action: "formSubmit" } },
} as const;
const SIMULATION_STAGES: SimulationStage[] = [
{
tree: {
root: "card",
state: DASH_STATE,
state: FORM_STATE,
elements: {
card: {
type: "Card",
props: { title: "Team Performance", maxWidth: "sm", centered: true },
props: { title: "Contact Us", maxWidth: "md" },
children: [],
},
},
@@ -83,74 +86,72 @@ const SIMULATION_STAGES: SimulationStage[] = [
{
tree: {
root: "card",
state: DASH_STATE,
state: FORM_STATE,
elements: {
card: {
type: "Card",
props: { title: "Team Performance", maxWidth: "sm", centered: true },
children: ["m1"],
props: { title: "Contact Us", maxWidth: "md" },
children: ["name"],
},
m1: METRIC_REVENUE,
name: NAME_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/m1","value":{"type":"Metric","props":{"label":"Weekly Revenue","value":"12,400","prefix":"$","change":"+18%","changeType":"positive"}}}',
'{"op":"add","path":"/elements/name","value":{"type":"Input","props":{"label":"Name","name":"name","statePath":"/form/name","checks":[{"type":"required","message":"Name is required"}]}}}',
},
{
tree: {
root: "card",
state: DASH_STATE,
state: FORM_STATE,
elements: {
card: {
type: "Card",
props: { title: "Team Performance", maxWidth: "sm", centered: true },
children: ["m1", "chart"],
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email"],
},
m1: METRIC_REVENUE,
chart: CHART,
name: NAME_INPUT,
email: EMAIL_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/chart","value":{"type":"LineGraph","props":{"data":{"$state":"/chartData"}}}}',
'{"op":"add","path":"/elements/email","value":{"type":"Input","props":{"label":"Email","name":"email","type":"email","statePath":"/form/email","checks":[{"type":"required","message":"Email is required"},{"type":"email","message":"Please enter a valid email"}]}}}',
},
{
tree: {
root: "card",
state: DASH_STATE,
state: FORM_STATE,
elements: {
card: {
type: "Card",
props: { title: "Team Performance", maxWidth: "sm", centered: true },
children: ["m1", "chart", "sep", "p1"],
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email", "message"],
},
m1: METRIC_REVENUE,
chart: CHART,
sep: SEP,
p1: PROGRESS_DEALS,
name: NAME_INPUT,
email: EMAIL_INPUT,
message: MESSAGE_INPUT,
},
},
stream:
'{"op":"add","path":"/elements/p1","value":{"type":"Progress","props":{"value":72,"label":"Deals Closed -- 72%"}}}',
'{"op":"add","path":"/elements/message","value":{"type":"Textarea","props":{"label":"Message","name":"message","statePath":"/form/message","checks":[{"type":"required","message":"Message is required"}]}}}',
},
{
tree: {
root: "card",
state: DASH_STATE,
state: FORM_STATE,
elements: {
card: {
type: "Card",
props: { title: "Team Performance", maxWidth: "sm", centered: true },
children: ["m1", "chart", "sep", "p1", "p2"],
props: { title: "Contact Us", maxWidth: "md" },
children: ["name", "email", "message", "submit"],
},
m1: METRIC_REVENUE,
chart: CHART,
sep: SEP,
p1: PROGRESS_DEALS,
p2: PROGRESS_RETENTION,
name: NAME_INPUT,
email: EMAIL_INPUT,
message: MESSAGE_INPUT,
submit: SUBMIT_BUTTON,
},
},
stream:
'{"op":"add","path":"/elements/p2","value":{"type":"Progress","props":{"value":91,"label":"Retention -- 91%"}}}',
'{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Send Message","variant":"primary"},"on":{"press":{"action":"formSubmit"}}}}',
},
];
@@ -210,10 +211,10 @@ function specToNested(spec: Spec): Record<string, unknown> {
}
const EXAMPLE_PROMPTS = [
"Recipe card with rating and ingredients",
"Order receipt with item list and total",
"Team member profile card",
"Notification inbox with alerts",
"Create a login form with email and password",
"Build a feedback form with rating stars",
"Design a contact card with avatar",
"Make a settings panel with toggles",
];
export function Demo({
@@ -1116,7 +1117,7 @@ Open [http://localhost:3000](http://localhost:3000) to view.
))}
</div>
<div
className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[36rem]"}`}
className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
{activeTab !== "catalog" && (
<div className="absolute top-2 right-2 z-10">
@@ -1356,7 +1357,7 @@ Open [http://localhost:3000](http://localhost:3000) to view.
</div>
</div>
<div
className={`border border-border rounded bg-background grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[36rem]"}`}
className={`border border-border rounded bg-background grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[28rem]"}`}
>
{renderView === "static" && (
<div className="absolute top-2 right-2 z-10">
+5 -11
View File
@@ -19,13 +19,7 @@ const navLinks = [
{ href: "/docs", label: "Docs" },
];
function GitHubLink({
className,
stars,
}: {
className?: string;
stars?: string;
}) {
function GitHubLink({ className }: { className?: string }) {
return (
<a
href="https://github.com/vercel-labs/json-render"
@@ -44,12 +38,12 @@ function GitHubLink({
>
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
</svg>
{stars && <span>{stars}</span>}
<span>12k</span>
</a>
);
}
export function Header({ stars }: { stars?: string }) {
export function Header() {
const pathname = usePathname();
const [mobileOpen, setMobileOpen] = useState(false);
@@ -128,14 +122,14 @@ export function Header({ stars }: { stars?: string }) {
</Link>
))}
<Search />
<GitHubLink stars={stars} />
<GitHubLink />
<ThemeToggle />
</nav>
{/* Mobile nav */}
<div className="flex sm:hidden items-center gap-3">
<Search />
<GitHubLink stars={stars} />
<GitHubLink />
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
<SheetTrigger
className="flex items-center justify-center"
-16
View File
@@ -108,22 +108,6 @@ export const examples: Example[] = [
githubPath: "examples/react-three-fiber",
demoUrl: "https://react-three-fiber-demo.json-render.dev",
},
{
slug: "gsplat",
title: "Gaussian Splatting (experimental)",
description:
"Experimental standalone gaussian splat viewer using gsplat.js — no Three.js dependency.",
tags: ["React", "3D", "Next.js"],
githubPath: "examples/gsplat",
},
{
slug: "react-three-fiber-gsplat",
title: "R3F + Gaussian Splatting",
description:
"Gaussian splats composed with 3D primitives and lights in React Three Fiber scenes.",
tags: ["React", "3D", "Next.js"],
githubPath: "examples/react-three-fiber-gsplat",
},
{
slug: "react-native",
title: "React Native",
-20
View File
@@ -1,20 +0,0 @@
const REPO = "vercel-labs/json-render";
const REVALIDATE = 86400;
export async function getStarCount(): Promise<string> {
try {
const res = await fetch(`https://api.github.com/repos/${REPO}`, {
headers: { Accept: "application/vnd.github.v3+json" },
next: { revalidate: REVALIDATE },
});
if (!res.ok) return "";
const data = await res.json();
const count = data.stargazers_count;
if (typeof count !== "number") return "";
if (count >= 1000)
return `${(count / 1000).toFixed(count >= 10000 ? 0 : 1)}k`;
return String(count);
} catch {
return "";
}
}
+4 -44
View File
@@ -165,8 +165,7 @@ export const playgroundCatalog = defineCatalog(schema, {
.enum(["body", "caption", "muted", "lead", "code"])
.nullable(),
}),
description:
'Paragraph text. In repeat scopes, use { "$template": "${field1} ${field2}" } to interpolate item fields.',
description: "Paragraph text",
example: { text: "Hello, world!" },
},
@@ -179,18 +178,6 @@ export const playgroundCatalog = defineCatalog(schema, {
description: "Placeholder image (displays alt text in a styled box)",
},
Icon: {
props: z.object({
name: z.string(),
size: z.enum(["sm", "md", "lg"]).nullable(),
color: z
.enum(["default", "muted", "primary", "success", "warning", "danger"])
.nullable(),
}),
description:
"Lucide icon by name. PascalCase: MapPin, Mail, Globe, Calendar, Star, Heart, Check, X, ArrowRight, Phone, Building, Clock, Shield, Zap, Users, Eye, Download, Upload, Search, Filter, Settings, Bell, ChevronRight, ExternalLink, Info, AlertTriangle, CheckCircle, XCircle. Use in horizontal Stacks with Text for icon+label patterns. Never use emoji — always use Icon.",
},
Avatar: {
props: z.object({
src: z.string().nullable(),
@@ -272,32 +259,8 @@ export const playgroundCatalog = defineCatalog(schema, {
value: z.number(),
max: z.number().nullable(),
label: z.string().nullable(),
interactive: z.boolean().nullable(),
}),
events: ["change"],
description:
"Interactive star rating. Use { $bindState } on value for binding. Set interactive: false for read-only display.",
example: { value: 4, max: 5, label: "Rating" },
},
Metric: {
props: z.object({
label: z.string(),
value: z.string(),
change: z.string().nullable(),
changeType: z.enum(["positive", "negative", "neutral"]).nullable(),
prefix: z.string().nullable(),
suffix: z.string().nullable(),
}),
description:
"Key metric / stat display. Shows a large value with label and optional change indicator. Use for dashboard KPIs.",
example: {
label: "Total Revenue",
value: "125,000",
prefix: "$",
change: "+12.5%",
changeType: "positive",
},
description: "Star rating display",
},
// ── Charts ──────────────────────────────────────────────────────────
@@ -448,14 +411,11 @@ export const playgroundCatalog = defineCatalog(schema, {
Button: {
props: z.object({
label: z.string(),
variant: z
.enum(["primary", "secondary", "outline", "danger"])
.nullable(),
variant: z.enum(["primary", "secondary", "danger"]).nullable(),
disabled: z.boolean().nullable(),
}),
events: ["press"],
description:
"Clickable button. primary = solid fill, outline = bordered/transparent, secondary = muted fill. Bind on.press for handler.",
description: "Clickable button. Bind on.press for handler.",
example: { label: "Submit", variant: "primary" },
},
+125 -356
View File
@@ -105,7 +105,6 @@ import {
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { icons as lucideIcons } from "lucide-react";
// =============================================================================
// Registry — components + actions, types inferred from catalog
@@ -128,25 +127,23 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
return (
<div
className={`border border-border rounded-xl p-5 bg-card text-card-foreground shadow-sm overflow-hidden h-full flex flex-col ${maxWidthClass} ${centeredClass}`}
className={`border border-border rounded-lg p-4 bg-card text-card-foreground overflow-hidden ${maxWidthClass} ${centeredClass}`}
>
{(props.title || props.description) && (
<div className="mb-4">
{props.title && (
<h3 className="font-semibold text-lg tracking-tight">
<h3 className="font-semibold text-lg text-left">
{props.title}
</h3>
)}
{props.description && (
<p className="text-sm text-muted-foreground mt-1">
<p className="text-sm text-muted-foreground mt-1 text-left">
{props.description}
</p>
)}
</div>
)}
<div className="flex-1 flex flex-col gap-4 [&>:last-child]:mt-auto">
{children}
</div>
<div className="space-y-3">{children}</div>
</div>
);
},
@@ -163,31 +160,14 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
: props.gap === "none"
? "gap-0"
: "gap-3";
let alignClass: string;
if (isHorizontal) {
alignClass =
props.align === "center"
? "items-center"
: props.align === "end"
? "items-end"
: props.align === "stretch"
? "items-stretch"
: "items-start";
} else {
// Vertical: items-center/end lets inline elements (Avatar, Badge, Button)
// center/align naturally. Block containers (Grid, Accordion, Table) add
// their own w-full to stretch regardless.
alignClass =
props.align === "center"
? "items-center"
: props.align === "end"
? "items-end"
: props.align === "start"
? "items-start"
: "items-stretch";
}
const alignClass =
props.align === "center"
? "items-center"
: props.align === "end"
? "items-end"
: props.align === "stretch"
? "items-stretch"
: "items-start";
const justifyClass =
props.justify === "center"
? "justify-center"
@@ -201,7 +181,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
return (
<div
className={`flex ${isHorizontal ? "flex-row flex-wrap" : "flex-col w-full"} ${gapClass} ${alignClass} ${justifyClass}`}
className={`flex ${isHorizontal ? "flex-row flex-wrap" : "flex-col"} ${gapClass} ${alignClass} ${justifyClass}`}
>
{children}
</div>
@@ -209,12 +189,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
},
Grid: ({ props, children }) => {
const childCount = Array.isArray(children)
? children.length
: children
? 1
: 0;
const n = Math.min(props.columns ?? 1, childCount || 1);
const n = props.columns ?? 1;
const cols =
n >= 6
? "grid-cols-6"
@@ -230,15 +205,13 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const gridGap =
props.gap === "lg" ? "gap-4" : props.gap === "sm" ? "gap-2" : "gap-3";
return <div className={`grid w-full ${cols} ${gridGap}`}>{children}</div>;
return <div className={`grid ${cols} ${gridGap}`}>{children}</div>;
},
Separator: ({ props }) => (
<Separator
orientation={props.orientation ?? "horizontal"}
className={
props.orientation === "vertical" ? "h-full mx-3" : "my-4 opacity-50"
}
className={props.orientation === "vertical" ? "h-full mx-2" : "my-3"}
/>
),
@@ -410,7 +383,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
});
return (
<div className="w-full rounded-md border border-border overflow-hidden">
<div className="rounded-md border border-border overflow-hidden">
<TablePrimitive>
{props.caption && <TableCaption>{props.caption}</TableCaption>}
<TableHeader>
@@ -438,17 +411,20 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const level = props.level ?? "h2";
const headingClass =
level === "h1"
? "text-2xl font-bold tracking-tight"
? "text-2xl font-bold"
: level === "h3"
? "text-base font-semibold tracking-tight"
? "text-base font-semibold"
: level === "h4"
? "text-sm font-medium uppercase tracking-wider text-muted-foreground"
: "text-xl font-semibold tracking-tight";
? "text-sm font-semibold"
: "text-lg font-semibold";
if (level === "h1") return <h1 className={headingClass}>{props.text}</h1>;
if (level === "h3") return <h3 className={headingClass}>{props.text}</h3>;
if (level === "h4") return <h4 className={headingClass}>{props.text}</h4>;
return <h2 className={headingClass}>{props.text}</h2>;
if (level === "h1")
return <h1 className={`${headingClass} text-left`}>{props.text}</h1>;
if (level === "h3")
return <h3 className={`${headingClass} text-left`}>{props.text}</h3>;
if (level === "h4")
return <h4 className={`${headingClass} text-left`}>{props.text}</h4>;
return <h2 className={`${headingClass} text-left`}>{props.text}</h2>;
},
Text: ({ props }) => {
@@ -464,59 +440,20 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
: "text-sm";
if (props.variant === "code") {
return <code className={textClass}>{props.text}</code>;
return <code className={`${textClass} text-left`}>{props.text}</code>;
}
return <p className={textClass}>{props.text}</p>;
return <p className={`${textClass} text-left`}>{props.text}</p>;
},
Image: ({ props }) => (
<div
className="w-full bg-muted/50 border border-dashed border-border rounded-lg flex flex-col items-center justify-center gap-2 text-muted-foreground/60 px-4"
style={{
maxWidth: props.width ?? undefined,
height: props.height ?? 120,
minHeight: 80,
}}
className="bg-muted border border-border rounded flex items-center justify-center text-xs text-muted-foreground aspect-video"
style={{ width: props.width ?? 80, height: props.height ?? 60 }}
>
<svg
width="32"
height="32"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className="opacity-50"
>
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
<circle cx="9" cy="9" r="2" />
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
</svg>
{props.alt && <span className="text-xs">{props.alt}</span>}
{props.alt || "img"}
</div>
),
Icon: ({ props }) => {
const IconComponent = lucideIcons[props.name as keyof typeof lucideIcons];
if (!IconComponent) return null;
const sizeMap = { sm: 16, md: 20, lg: 24 } as const;
const px = sizeMap[props.size ?? "md"] ?? 20;
const colorClass =
props.color === "muted"
? "text-muted-foreground"
: props.color === "primary"
? "text-primary"
: props.color === "success"
? "text-green-600 dark:text-green-400"
: props.color === "warning"
? "text-yellow-600 dark:text-yellow-400"
: props.color === "danger"
? "text-red-600 dark:text-red-400"
: "";
return <IconComponent size={px} className={`shrink-0 ${colorClass}`} />;
},
Avatar: ({ props }) => {
const name = props.name || "?";
const initials = name
@@ -525,16 +462,16 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
.join("")
.slice(0, 2)
.toUpperCase();
const sizeStyles =
const avatarSize =
props.size === "lg"
? { outer: "w-[72px] h-[72px]", text: "text-xl", ring: "ring-[3px]" }
? "w-12 h-12 text-base"
: props.size === "sm"
? { outer: "w-8 h-8", text: "text-xs", ring: "ring-2" }
: { outer: "w-10 h-10", text: "text-sm", ring: "ring-2" };
? "w-8 h-8 text-xs"
: "w-10 h-10 text-sm";
return (
<div
className={`${sizeStyles.outer} ${sizeStyles.text} rounded-full bg-gradient-to-br from-muted-foreground/20 to-muted flex items-center justify-center font-semibold tracking-wide ${sizeStyles.ring} ring-background shadow-sm`}
className={`${avatarSize} rounded-full bg-muted flex items-center justify-center font-medium`}
>
{initials}
</div>
@@ -554,20 +491,9 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
: props.variant === "warning"
? "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-100"
: "";
const dotColor =
props.variant === "success"
? "bg-green-500"
: props.variant === "warning"
? "bg-yellow-500"
: props.variant === "danger"
? "bg-red-500"
: "";
return (
<Badge variant={variant} className={`${customClass} gap-1.5`}>
{dotColor && (
<span className={`w-1.5 h-1.5 rounded-full ${dotColor} shrink-0`} />
)}
<Badge variant={variant} className={customClass}>
{props.text}
</Badge>
);
@@ -584,47 +510,8 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
? "border-blue-200 bg-blue-50 text-blue-900 dark:border-blue-800 dark:bg-blue-950 dark:text-blue-100"
: "";
const iconProps = {
width: 16,
height: 16,
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
strokeWidth: 2,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
className: "shrink-0",
};
const icon =
props.type === "success" ? (
<svg {...iconProps}>
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
) : props.type === "warning" ? (
<svg {...iconProps}>
<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
) : props.type === "error" ? (
<svg {...iconProps}>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
) : (
<svg {...iconProps}>
<circle cx="12" cy="12" r="10" />
<line x1="12" y1="16" x2="12" y2="12" />
<line x1="12" y1="8" x2="12.01" y2="8" />
</svg>
);
return (
<Alert variant={variant} className={customClass}>
{icon}
<AlertTitle>{props.title}</AlertTitle>
{props.message && (
<AlertDescription>{props.message}</AlertDescription>
@@ -636,7 +523,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
Progress: ({ props }) => {
const value = Math.min(100, Math.max(0, props.value || 0));
return (
<div className="w-full space-y-2">
<div className="space-y-2">
{props.label && (
<Label className="text-sm text-muted-foreground">
{props.label}
@@ -720,19 +607,9 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
</PopoverPrimitive>
),
Rating: ({ props, bindings, emit }) => {
const [boundValue, setBoundValue] = useBoundProp<number>(
props.value as number | undefined,
bindings?.value,
);
const [localValue, setLocalValue] = useState(props.value || 0);
const isBound = !!bindings?.value;
const ratingValue = isBound ? (boundValue ?? 0) : localValue;
const setValue = isBound ? setBoundValue : setLocalValue;
Rating: ({ props }) => {
const ratingValue = props.value || 0;
const maxRating = props.max ?? 5;
const interactive = props.interactive !== false;
const [hoverIndex, setHoverIndex] = useState(-1);
return (
<div className="space-y-2">
{props.label && (
@@ -740,78 +617,15 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
{props.label}
</Label>
)}
<div
className="flex gap-0.5"
onMouseLeave={() => interactive && setHoverIndex(-1)}
>
{Array.from({ length: maxRating }).map((_, i) => {
const filled =
hoverIndex >= 0 ? i <= hoverIndex : i < ratingValue;
return (
<button
key={i}
type="button"
className={`p-0.5 transition-colors ${interactive ? "cursor-pointer hover:scale-110" : "cursor-default"}`}
onMouseEnter={() => interactive && setHoverIndex(i)}
onClick={() => {
if (!interactive) return;
const newVal = i + 1 === ratingValue ? 0 : i + 1;
setValue(newVal);
emit("change");
}}
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
className={
filled ? "text-yellow-400" : "text-muted-foreground/40"
}
>
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
</svg>
</button>
);
})}
</div>
</div>
);
},
Metric: ({ props }) => {
const changeColor =
props.changeType === "positive"
? "text-green-600 dark:text-green-400"
: props.changeType === "negative"
? "text-red-600 dark:text-red-400"
: "text-muted-foreground";
const changeIcon =
props.changeType === "positive"
? "\u2191"
: props.changeType === "negative"
? "\u2193"
: "";
return (
<div className="space-y-1">
<div className="text-sm text-muted-foreground">{props.label}</div>
<div className="flex items-baseline gap-1.5">
<span className="text-2xl font-semibold tracking-tight tabular-nums">
{props.prefix}
{props.value}
{props.suffix}
</span>
{props.change && (
<span className={`text-sm font-medium ${changeColor}`}>
{changeIcon}
{props.change}
<div className="flex gap-1">
{Array.from({ length: maxRating }).map((_, i) => (
<span
key={i}
className={`text-lg ${i < ratingValue ? "text-yellow-400" : "text-muted"}`}
>
*
</span>
)}
))}
</div>
</div>
);
@@ -822,39 +636,26 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
BarGraph: ({ props }) => {
const data = props.data || [];
const maxValue = Math.max(...data.map((d) => d.value), 1);
const barColors = [
"bg-primary",
"bg-primary/80",
"bg-primary/60",
"bg-primary/70",
"bg-primary/90",
"bg-primary/50",
];
return (
<div className="space-y-3">
<div className="space-y-2">
{props.title && (
<div className="text-sm font-medium">{props.title}</div>
<div className="text-sm font-medium text-left">{props.title}</div>
)}
<div className="flex items-end gap-2" style={{ height: 160 }}>
<div className="flex gap-2">
{data.map((d, i) => (
<div
key={i}
className="flex-1 flex flex-col items-center gap-1.5 h-full justify-end group"
>
<div className="text-[11px] font-medium text-muted-foreground tabular-nums">
{d.value}
</div>
<div className="w-full flex-1 flex items-end">
<div key={i} className="flex-1 flex flex-col items-center gap-1">
<div className="text-xs text-muted-foreground">{d.value}</div>
<div className="w-full h-24 flex items-end">
<div
className={`w-full ${barColors[i % barColors.length]} rounded-t-md transition-all group-hover:opacity-80`}
className="w-full bg-primary rounded-t transition-all"
style={{
height: `${(d.value / maxValue) * 100}%`,
minHeight: 4,
minHeight: 2,
}}
/>
</div>
<div className="text-[11px] text-muted-foreground truncate w-full text-center">
<div className="text-xs text-muted-foreground truncate w-full text-center">
{d.label}
</div>
</div>
@@ -871,8 +672,8 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const range = maxValue - minValue || 1;
const width = 300;
const height = 140;
const padding = { top: 12, right: 12, bottom: 12, left: 12 };
const height = 100;
const padding = { top: 10, right: 10, bottom: 10, left: 10 };
const chartWidth = width - padding.left - padding.right;
const chartHeight = height - padding.top - padding.bottom;
@@ -889,107 +690,77 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
return { x, y, ...d };
});
// Build smooth cubic bezier curve through points
let smoothPath = "";
let areaPath = "";
if (points.length > 1) {
const first = points[0]!;
const last = points[points.length - 1]!;
smoothPath = `M ${first.x} ${first.y}`;
for (let i = 0; i < points.length - 1; i++) {
const curr = points[i]!;
const next = points[i + 1]!;
const cpx = (curr.x + next.x) / 2;
smoothPath += ` C ${cpx} ${curr.y}, ${cpx} ${next.y}, ${next.x} ${next.y}`;
}
const bottomY = height - padding.bottom;
areaPath = `${smoothPath} L ${last.x} ${bottomY} L ${first.x} ${bottomY} Z`;
} else if (points.length === 1) {
const only = points[0]!;
smoothPath = `M ${only.x} ${only.y}`;
}
const gradientId = `line-gradient-${Math.random().toString(36).slice(2, 8)}`;
const pathD =
points.length > 0
? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}`
: "";
return (
<div className="space-y-3">
<div className="space-y-2">
{props.title && (
<div className="text-sm font-medium">{props.title}</div>
<div className="text-sm font-medium text-left">{props.title}</div>
)}
<div className="relative" style={{ height: 160 }}>
<svg
viewBox={`0 0 ${width} ${height}`}
className="w-full h-full"
preserveAspectRatio="none"
>
<defs>
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
<stop
offset="0%"
stopColor="currentColor"
stopOpacity="0.15"
/>
<stop
offset="100%"
stopColor="currentColor"
stopOpacity="0"
/>
</linearGradient>
</defs>
{[0, 0.25, 0.5, 0.75, 1].map((frac) => (
<line
key={frac}
x1={padding.left}
y1={padding.top + chartHeight * frac}
x2={width - padding.right}
y2={padding.top + chartHeight * frac}
stroke="currentColor"
strokeOpacity="0.07"
vectorEffect="non-scaling-stroke"
strokeWidth="1"
/>
))}
{areaPath && (
<div className="relative h-28">
<svg viewBox={`0 0 ${width} ${height}`} className="w-full h-full">
<line
x1={padding.left}
y1={padding.top + chartHeight / 2}
x2={width - padding.right}
y2={padding.top + chartHeight / 2}
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
<line
x1={padding.left}
y1={padding.top}
x2={width - padding.right}
y2={padding.top}
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
<line
x1={padding.left}
y1={height - padding.bottom}
x2={width - padding.right}
y2={height - padding.bottom}
stroke="currentColor"
strokeOpacity="0.1"
strokeWidth="1"
/>
{pathD && (
<path
d={areaPath}
fill={`url(#${gradientId})`}
className="text-primary"
/>
)}
{smoothPath && (
<path
d={smoothPath}
d={pathD}
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
className="text-primary"
/>
)}
</svg>
{points.map((p, i) => (
<div
key={i}
className="absolute w-[7px] h-[7px] rounded-full bg-primary -translate-x-1/2 -translate-y-1/2"
style={{
left: `${(p.x / width) * 100}%`,
top: `${(p.y / height) * 100}%`,
}}
/>
))}
</div>
{points.length > 0 && (
<div className="relative h-4">
{points.map((p, i) => (
<span
<circle
key={i}
className="absolute text-[11px] text-muted-foreground -translate-x-1/2"
style={{ left: `${(p.x / width) * 100}%` }}
cx={p.x}
cy={p.y}
r="4"
className="fill-primary"
/>
))}
</svg>
</div>
{data.length > 0 && (
<div className="flex justify-between">
{data.map((d, i) => (
<div
key={i}
className="text-xs text-muted-foreground text-center"
style={{ width: `${100 / data.length}%` }}
>
{data[i]?.label}
</span>
{d.label}
</div>
))}
</div>
)}
@@ -1016,7 +787,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
);
return (
<div className="w-full space-y-2">
<div className="space-y-2">
{props.label && <Label htmlFor={props.name}>{props.label}</Label>}
<Input
id={props.name}
@@ -1058,7 +829,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
);
return (
<div className="w-full space-y-2">
<div className="space-y-2">
{props.label && <Label htmlFor={props.name}>{props.label}</Label>}
<Textarea
id={props.name}
@@ -1101,7 +872,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
);
return (
<div className="w-full space-y-2">
<div className="space-y-2">
<Label>{props.label}</Label>
<Select
value={value}
@@ -1174,7 +945,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const setValue = isBound ? setBoundValue : setLocalValue;
return (
<div className="w-full space-y-2">
<div className="space-y-2">
{props.label && <Label>{props.label}</Label>}
<RadioGroup
value={value}
@@ -1216,7 +987,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const setChecked = isBound ? setBoundChecked : setLocalChecked;
return (
<div className="w-full flex items-center justify-between space-x-2">
<div className="flex items-center justify-between space-x-2">
<Label htmlFor={props.name} className="cursor-pointer">
{props.label}
</Label>
@@ -1243,7 +1014,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const setValue = isBound ? setBoundValue : setLocalValue;
return (
<div className="w-full space-y-2">
<div className="space-y-2">
{props.label && (
<div className="flex justify-between">
<Label className="text-sm">{props.label}</Label>
@@ -1270,11 +1041,9 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
const variant =
props.variant === "danger"
? "destructive"
: props.variant === "outline"
? "outline"
: props.variant === "secondary"
? "secondary"
: "default";
: props.variant === "secondary"
? "secondary"
: "default";
return (
<Button
+1 -1
View File
@@ -47,7 +47,7 @@ export function Header() {
</div>
<nav className="flex items-center gap-4">
<a
href="https://json-render.dev"
href="https://json-render.vercel.app"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
-421
View File
@@ -1,421 +0,0 @@
"use client";
import { useEffect, useRef, useState, type ReactNode } from "react";
import * as SPLAT from "gsplat";
type Vec3 = [number, number, number];
interface SplatEntry {
src: string;
position?: Vec3;
rotation?: Vec3;
scale?: Vec3;
visible?: boolean;
}
interface ViewerProps {
width?: string;
height?: string;
backgroundColor?: string;
controls?: boolean;
autoRotate?: boolean;
autoRotateSpeed?: number;
cameraPosition?: Vec3 | null;
cameraTarget?: Vec3 | null;
fov?: number | null;
progressBarColor?: string;
progressTrackColor?: string;
progressTextColor?: string;
progressBackgroundColor?: string;
/** Custom loading indicator — overrides the default progress bar */
loadingIndicator?: ReactNode;
/** Splat file URLs to load */
splats?: SplatEntry[];
children?: ReactNode;
}
interface ProgressIndicatorProps {
progress: number;
backgroundColor?: string;
textColor?: string;
barColor?: string;
trackColor?: string;
}
function ProgressIndicator({
progress,
backgroundColor = "#0a0a0a",
textColor = "#666",
barColor = "#fff",
trackColor = "#1e1e1e",
}: ProgressIndicatorProps) {
const pct = Math.round(progress * 100);
return (
<div
style={{
position: "absolute",
inset: 0,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
background: backgroundColor,
gap: 12,
}}
>
<span
aria-live="polite"
style={{
color: textColor,
fontFamily: "ui-monospace, monospace",
fontSize: 13,
letterSpacing: "0.04em",
}}
>
Loading splat... {pct}%
</span>
<div
role="progressbar"
aria-valuenow={pct}
aria-valuemin={0}
aria-valuemax={100}
aria-label="Loading progress"
style={{
width: 200,
height: 2,
background: trackColor,
borderRadius: 1,
overflow: "hidden",
}}
>
<div
style={{
width: `${pct}%`,
height: "100%",
background: barColor,
borderRadius: 1,
transition: "width 150ms ease-out",
}}
/>
</div>
</div>
);
}
/** Convert a vertical FOV (degrees) to a focal length given a canvas height. */
function fovToFocalLength(fovDeg: number, height: number): number {
return height / (2 * Math.tan((fovDeg * Math.PI) / 360));
}
/** Convert euler angles (degrees, XYZ order) to a Quaternion. */
function eulerToQuaternion(euler: Vec3): SPLAT.Quaternion {
const [ex, ey, ez] = euler.map((d) => (d * Math.PI) / 180) as [
number,
number,
number,
];
const cx = Math.cos(ex / 2),
sx = Math.sin(ex / 2);
const cy = Math.cos(ey / 2),
sy = Math.sin(ey / 2);
const cz = Math.cos(ez / 2),
sz = Math.sin(ez / 2);
return new SPLAT.Quaternion(
sx * cy * cz + cx * sy * sz,
cx * sy * cz - sx * cy * sz,
cx * cy * sz + sx * sy * cz,
cx * cy * cz - sx * sy * sz,
);
}
/**
* Container that manages a WebGL canvas and loads gaussian splats
* using Hugging Face's gsplat.js — a standalone WebGL renderer (no Three.js).
*
* This is an experimental demo component, kept inline in the example app.
*/
export function GaussianSplatViewer({
width = "100%",
height = "100%",
backgroundColor = "#000000",
controls: enableControls = true,
autoRotate = false,
autoRotateSpeed = 1,
cameraPosition = null,
cameraTarget = null,
fov = null,
progressBarColor,
progressTrackColor,
progressTextColor,
progressBackgroundColor,
children,
loadingIndicator,
splats,
}: ViewerProps) {
const containerRef = useRef<HTMLDivElement>(null);
const cleanupRef = useRef<(() => void) | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [progress, setProgress] = useState(0);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!containerRef.current) return;
const container = containerRef.current;
let cancelled = false;
async function init() {
try {
// Clean up previous viewer
if (cleanupRef.current) {
cleanupRef.current();
cleanupRef.current = null;
}
const scene = new SPLAT.Scene();
const camera = new SPLAT.Camera();
const renderer = new SPLAT.WebGLRenderer();
// Apply camera position
if (cameraPosition) {
camera.position = new SPLAT.Vector3(
cameraPosition[0],
cameraPosition[1],
cameraPosition[2],
);
}
// Apply FOV by converting to focal length
const rect = container.getBoundingClientRect();
if (fov) {
const fl = fovToFocalLength(fov, rect.height);
camera.data.fx = fl;
camera.data.fy = fl;
}
// Size the canvas to the container
renderer.setSize(rect.width, rect.height);
// Style and append the canvas — absolute positioning prevents overflow
renderer.canvas.style.position = "absolute";
renderer.canvas.style.top = "0";
renderer.canvas.style.left = "0";
renderer.canvas.style.width = "100%";
renderer.canvas.style.height = "100%";
renderer.canvas.style.display = "block";
container.appendChild(renderer.canvas);
// Only create orbit controls if enabled
let controls: SPLAT.OrbitControls | null = null;
if (enableControls) {
controls = new SPLAT.OrbitControls(camera, renderer.canvas);
// Apply camera target (look-at)
if (cameraTarget) {
controls.setCameraTarget(
new SPLAT.Vector3(
cameraTarget[0],
cameraTarget[1],
cameraTarget[2],
),
);
}
}
// Handle resize (debounced to avoid layout thrashing)
let resizeTimer: ReturnType<typeof setTimeout>;
const onResize = () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
const r = container.getBoundingClientRect();
renderer.setSize(r.width, r.height);
// Update focal length on resize to maintain FOV
if (fov) {
const fl = fovToFocalLength(fov, r.height);
camera.data.fx = fl;
camera.data.fy = fl;
}
}, 100);
};
window.addEventListener("resize", onResize);
// Load splat files with progress tracking
if (splats && splats.length > 0) {
const totalSplats = splats.length;
for (let i = 0; i < totalSplats; i++) {
if (cancelled) return;
const entry = splats[i]!;
const splatObject = await SPLAT.Loader.LoadAsync(
entry.src,
scene,
(p: number) => {
if (!cancelled) {
const overallProgress = (i + p) / totalSplats;
// Use Math.max to prevent progress bar from jumping backwards
setProgress((prev) => Math.max(prev, overallProgress));
}
},
);
// Apply per-splat transforms
if (entry.position) {
splatObject.position = new SPLAT.Vector3(
entry.position[0],
entry.position[1],
entry.position[2],
);
}
if (entry.rotation) {
splatObject.rotation = eulerToQuaternion(entry.rotation);
}
if (entry.scale) {
splatObject.scale = new SPLAT.Vector3(
entry.scale[0],
entry.scale[1],
entry.scale[2],
);
}
if (entry.visible !== undefined) {
splatObject.visible = entry.visible;
}
}
}
if (cancelled) {
controls?.dispose();
renderer.dispose();
if (renderer.canvas.parentElement) {
renderer.canvas.parentElement.removeChild(renderer.canvas);
}
window.removeEventListener("resize", onResize);
return;
}
setIsLoading(false);
setError(null);
// Render loop with optional auto-rotation
let animationId: number;
let lastTime = performance.now();
const frame = () => {
const now = performance.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
if (autoRotate) {
// Rotate the camera around the camera target (not world origin)
const speed = autoRotateSpeed * 0.5;
const angle = speed * dt;
const pos = camera.position;
const target = cameraTarget
? new SPLAT.Vector3(
cameraTarget[0],
cameraTarget[1],
cameraTarget[2],
)
: new SPLAT.Vector3(0, 0, 0);
const dx = pos.x - target.x;
const dz = pos.z - target.z;
const cos = Math.cos(angle);
const sin = Math.sin(angle);
camera.position = new SPLAT.Vector3(
target.x + dx * cos - dz * sin,
pos.y,
target.z + dx * sin + dz * cos,
);
}
controls?.update();
renderer.render(scene, camera);
animationId = requestAnimationFrame(frame);
};
animationId = requestAnimationFrame(frame);
// Store cleanup function
cleanupRef.current = () => {
cancelAnimationFrame(animationId);
clearTimeout(resizeTimer);
window.removeEventListener("resize", onResize);
controls?.dispose();
renderer.dispose();
if (renderer.canvas.parentElement) {
renderer.canvas.parentElement.removeChild(renderer.canvas);
}
};
} catch (err) {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Failed to initialize viewer",
);
setIsLoading(false);
}
}
}
init();
return () => {
cancelled = true;
if (cleanupRef.current) {
cleanupRef.current();
cleanupRef.current = null;
}
};
}, [
splats,
enableControls,
autoRotate,
autoRotateSpeed,
cameraPosition,
cameraTarget,
fov,
]);
return (
<div
style={{
width,
height,
position: "relative",
overflow: "hidden",
background: backgroundColor,
}}
>
<div
ref={containerRef}
style={{ position: "relative", width: "100%", height: "100%" }}
/>
{isLoading &&
(loadingIndicator ?? (
<ProgressIndicator
progress={progress}
backgroundColor={progressBackgroundColor}
barColor={progressBarColor}
trackColor={progressTrackColor}
textColor={progressTextColor}
/>
))}
{error && (
<div
role="alert"
style={{
position: "absolute",
inset: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
background: backgroundColor,
color: "#ff4444",
fontFamily: "ui-monospace, monospace",
fontSize: 12,
padding: 20,
textAlign: "center",
}}
>
{error}
</div>
)}
{children}
</div>
);
}
-79
View File
@@ -1,79 +0,0 @@
declare module "gsplat" {
export class Vector3 {
constructor(x?: number, y?: number, z?: number);
x: number;
y: number;
z: number;
}
export class Quaternion {
constructor(x?: number, y?: number, z?: number, w?: number);
x: number;
y: number;
z: number;
w: number;
}
export class Splat {
get position(): Vector3;
set position(position: Vector3);
get rotation(): Quaternion;
set rotation(rotation: Quaternion);
get scale(): Vector3;
set scale(scale: Vector3);
get visible(): boolean;
set visible(visible: boolean);
}
export class Scene {
constructor();
}
export class Camera {
constructor();
get position(): Vector3;
set position(position: Vector3);
get rotation(): Quaternion;
set rotation(rotation: Quaternion);
data: CameraData;
}
export class CameraData {
get fx(): number;
set fx(fx: number);
get fy(): number;
set fy(fy: number);
}
export class WebGLRenderer {
constructor(canvas?: HTMLCanvasElement);
canvas: HTMLCanvasElement;
setSize(width: number, height: number): void;
render(scene: Scene, camera: Camera): void;
dispose(): void;
}
export class OrbitControls {
constructor(camera: Camera, canvas: HTMLCanvasElement);
setCameraTarget(target: Vector3): void;
minAngle: number;
maxAngle: number;
minZoom: number;
maxZoom: number;
orbitSpeed: number;
panSpeed: number;
zoomSpeed: number;
dampening: number;
update(): void;
dispose(): void;
}
export class Loader {
static LoadAsync(
url: string,
scene: Scene,
onProgress?: (progress: number) => void,
useCache?: boolean,
): Promise<Splat>;
}
}
-21
View File
@@ -1,21 +0,0 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "json-render Gaussian Splatting Example",
description:
"Experimental gaussian splat viewer demo using gsplat.js (no Three.js)",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif" }}>
{children}
</body>
</html>
);
}
-460
View File
@@ -1,460 +0,0 @@
"use client";
import {
useCallback,
useEffect,
useRef,
useState,
useSyncExternalStore,
} from "react";
import { GaussianSplatViewer } from "./GaussianSplatViewer";
import { scenes } from "./scenes";
function highlightJson(json: string): string {
return json.replace(
/("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
(_, key, colon, str, num, lit, punct) => {
if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
if (str) return `<span style="color:#00CA50">${str}</span>`;
if (num) return `<span style="color:#47A8FF">${num}</span>`;
if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
if (punct) return `<span style="color:#666">${punct}</span>`;
return _;
},
);
}
const MOBILE_BREAKPOINT = 768;
function subscribeToResize(cb: () => void) {
window.addEventListener("resize", cb);
return () => window.removeEventListener("resize", cb);
}
function getIsMobile() {
return typeof window !== "undefined"
? window.innerWidth < MOBILE_BREAKPOINT
: false;
}
function useIsMobile() {
return useSyncExternalStore(subscribeToResize, getIsMobile, () => false);
}
const LIST_WIDTH = 220;
const JSON_WIDTH = 380;
const HEADER_HEIGHT = 40;
const headerStyle: React.CSSProperties = {
height: HEADER_HEIGHT,
display: "flex",
alignItems: "center",
padding: "0 16px",
fontSize: 11,
fontWeight: 600,
color: "#555",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: "ui-monospace, monospace",
borderBottom: "1px solid #1e1e1e",
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
boxSizing: "border-box",
};
function SplatViewer({ sceneIndex }: { sceneIndex: number }) {
const scene = scenes[sceneIndex]!;
const v = scene.viewer;
return (
<GaussianSplatViewer
width="100%"
height="100%"
backgroundColor={v.backgroundColor ?? "#0a0a0a"}
controls={v.controls ?? true}
autoRotate={v.autoRotate ?? true}
autoRotateSpeed={v.autoRotateSpeed ?? 0.5}
cameraPosition={v.cameraPosition ?? [0, 2, 5]}
cameraTarget={v.cameraTarget ?? [0, 0, 0]}
fov={v.fov ?? 50}
splats={scene.splats}
/>
);
}
function SceneListItem({
scene,
index,
isSelected,
onSelect,
}: {
scene: (typeof scenes)[number];
index: number;
isSelected: boolean;
onSelect: (i: number) => void;
}) {
return (
<button
role="option"
aria-selected={isSelected}
onClick={() => onSelect(index)}
style={{
display: "block",
width: "100%",
padding: "8px 16px",
fontSize: 13,
border: "none",
textAlign: "left",
background: isSelected ? "rgba(255,255,255,0.08)" : "transparent",
color: isSelected ? "#fff" : "#888",
fontWeight: isSelected ? 500 : 400,
cursor: "pointer",
borderLeft: isSelected ? "2px solid #fff" : "2px solid transparent",
fontFamily: "inherit",
}}
>
{scene.name}
</button>
);
}
function DesktopLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = scenes[selectedIndex]!;
return (
<div style={{ height: "100dvh", display: "flex", background: "#0a0a0a" }}>
<nav
aria-label="Splat scenes"
style={{
width: LIST_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderRight: "1px solid #1e1e1e",
background: "#0f0f0f",
}}
>
<div style={headerStyle}>Splat Scenes</div>
<div
role="listbox"
aria-label="Scene list"
style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}
>
{scenes.map((scene, i) => (
<SceneListItem
key={scene.name}
scene={scene}
index={i}
isSelected={i === selectedIndex}
onSelect={setSelectedIndex}
/>
))}
</div>
</nav>
<main
style={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0,
}}
>
<div style={headerStyle}>{selected.description}</div>
<div style={{ flex: 1, background: "#000", position: "relative" }}>
<SplatViewer key={selectedIndex} sceneIndex={selectedIndex} />
</div>
</main>
<aside
aria-label="Spec JSON"
style={{
width: JSON_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderLeft: "1px solid #1e1e1e",
background: "#0d0d0d",
}}
>
<div style={headerStyle}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(
JSON.stringify(
{ spec: selected.spec, splats: selected.splats },
null,
2,
),
),
}}
/>
</aside>
</div>
);
}
function MobileLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const [showJson, setShowJson] = useState(false);
const [showScenes, setShowScenes] = useState(false);
const selected = scenes[selectedIndex]!;
const scenePanelRef = useRef<HTMLDivElement>(null);
const jsonPanelRef = useRef<HTMLDivElement>(null);
const closeScenes = useCallback(() => setShowScenes(false), []);
const closeJson = useCallback(() => setShowJson(false), []);
// Close overlays on Escape
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape") {
if (showScenes) closeScenes();
if (showJson) closeJson();
}
};
window.addEventListener("keydown", onKeyDown);
return () => window.removeEventListener("keydown", onKeyDown);
}, [showScenes, showJson, closeScenes, closeJson]);
// Focus the panel when it opens
useEffect(() => {
if (showScenes) scenePanelRef.current?.focus();
}, [showScenes]);
useEffect(() => {
if (showJson) jsonPanelRef.current?.focus();
}, [showJson]);
return (
<div
style={{
height: "100dvh",
display: "flex",
flexDirection: "column",
background: "#0a0a0a",
overflow: "hidden",
}}
>
<header
style={{
height: 48,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 12px",
borderBottom: "1px solid #1e1e1e",
background: "#0f0f0f",
flexShrink: 0,
gap: 8,
}}
>
<button
aria-label={`Scene: ${selected.name}. Open scene list`}
aria-expanded={showScenes}
onClick={() => {
setShowScenes((v) => !v);
setShowJson(false);
}}
style={{
background: showScenes ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 12,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "inherit",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "50%",
}}
>
{selected.name}
</button>
<span
style={{
flex: 1,
fontSize: 10,
color: "#555",
textAlign: "center",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontFamily: "ui-monospace, monospace",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
{selected.description}
</span>
<button
aria-label="Toggle JSON panel"
aria-expanded={showJson}
onClick={() => {
setShowJson((v) => !v);
setShowScenes(false);
}}
style={{
background: showJson ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 11,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "ui-monospace, monospace",
whiteSpace: "nowrap",
letterSpacing: "0.04em",
}}
>
JSON
</button>
</header>
<div style={{ flex: 1, position: "relative", minHeight: 0 }}>
<SplatViewer key={selectedIndex} sceneIndex={selectedIndex} />
{showScenes && (
<>
<div
onClick={closeScenes}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
ref={scenePanelRef}
role="dialog"
aria-modal="true"
aria-label="Scene list"
tabIndex={-1}
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: "75%",
maxWidth: 280,
background: "#0f0f0f",
borderRight: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
overflowY: "auto",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Splat Scenes</div>
<div
role="listbox"
aria-label="Scene list"
style={{ padding: "6px 0" }}
>
{scenes.map((scene, i) => (
<SceneListItem
key={scene.name}
scene={scene}
index={i}
isSelected={i === selectedIndex}
onSelect={(idx) => {
setSelectedIndex(idx);
closeScenes();
}}
/>
))}
</div>
</div>
</>
)}
{showJson && (
<>
<div
onClick={closeJson}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
ref={jsonPanelRef}
role="dialog"
aria-modal="true"
aria-label="Spec JSON"
tabIndex={-1}
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "85%",
maxWidth: 400,
background: "#0d0d0d",
borderLeft: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(
JSON.stringify(
{ spec: selected.spec, splats: selected.splats },
null,
2,
),
),
}}
/>
</div>
</>
)}
</div>
</div>
);
}
export default function Page() {
const isMobile = useIsMobile();
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}
-24
View File
@@ -1,24 +0,0 @@
export type ViewerConfig = {
backgroundColor?: string;
controls?: boolean;
autoRotate?: boolean;
autoRotateSpeed?: number;
cameraPosition?: [number, number, number];
cameraTarget?: [number, number, number];
fov?: number;
};
// The "spec" here is just JSON shown next to the viewer for illustration —
// the example doesn't actually render specs, so we keep it as a plain object.
export type Scene = {
name: string;
description: string;
spec: Record<string, unknown>;
viewer: ViewerConfig;
splats: Array<{
src: string;
position?: [number, number, number];
rotation?: [number, number, number];
scale?: [number, number, number];
}>;
};
-41
View File
@@ -1,41 +0,0 @@
import type { Scene } from "./_helpers";
export const bicycle: Scene = {
name: "Bicycle",
description: "A bicycle scene captured with gaussian splatting",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.4,
cameraPosition: [3, 2, 5],
cameraTarget: [0, 0, 0],
fov: 55,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.4,
cameraPosition: [3, 2, 5],
cameraTarget: [0, 0, 0],
fov: 55,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bicycle/bicycle-7k.splat",
position: [0, 0, 0],
},
],
};
-41
View File
@@ -1,41 +0,0 @@
import type { Scene } from "./_helpers";
export const bonsai: Scene = {
name: "Bonsai",
description: "A bonsai tree captured with gaussian splatting",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.5,
cameraPosition: [0, 1.5, 3],
cameraTarget: [0, 0.5, 0],
fov: 50,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.5,
cameraPosition: [0, 1.5, 3],
cameraTarget: [0, 0.5, 0],
fov: 50,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [0, 0, 0],
},
],
};
-41
View File
@@ -1,41 +0,0 @@
import type { Scene } from "./_helpers";
export const garden: Scene = {
name: "Garden",
description: "A garden scene captured with gaussian splatting",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.3,
cameraPosition: [2, 2, 5],
cameraTarget: [0, 0, 0],
fov: 60,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.3,
cameraPosition: [2, 2, 5],
cameraTarget: [0, 0, 0],
fov: 60,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/garden/garden-7k.splat",
position: [0, 0, 0],
},
],
};
-17
View File
@@ -1,17 +0,0 @@
import type { Scene } from "./_helpers";
import { bonsai } from "./bonsai";
import { garden } from "./garden";
import { bicycle } from "./bicycle";
import { kitchen } from "./kitchen";
import { stump } from "./stump";
import { multiOffset } from "./multi-offset";
export type { Scene };
export const scenes: Scene[] = [
bonsai,
garden,
bicycle,
kitchen,
stump,
multiOffset,
];
-41
View File
@@ -1,41 +0,0 @@
import type { Scene } from "./_helpers";
export const kitchen: Scene = {
name: "Kitchen",
description: "An indoor kitchen scan showing spatial depth and lighting",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#0d0d0d",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.3,
cameraPosition: [2, 2, 4],
cameraTarget: [0, 0.5, 0],
fov: 55,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#0d0d0d",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.3,
cameraPosition: [2, 2, 4],
cameraTarget: [0, 0.5, 0],
fov: 55,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/kitchen/kitchen-7k.splat",
position: [0, 0, 0],
},
],
};
@@ -1,47 +0,0 @@
import type { Scene } from "./_helpers";
export const multiOffset: Scene = {
name: "Multi-Splat (Offset)",
description:
"Two splats placed away from the origin — demonstrates off-center orbit",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.4,
cameraPosition: [5, 3, 8],
cameraTarget: [3, 0.5, 3],
fov: 50,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#0a0a0a",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.4,
cameraPosition: [5, 3, 8],
cameraTarget: [3, 0.5, 3],
fov: 50,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [1.5, 0, 3],
},
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/stump/stump-7k.splat",
position: [4.5, 0, 3],
scale: [0.8, 0.8, 0.8],
},
],
};
-41
View File
@@ -1,41 +0,0 @@
import type { Scene } from "./_helpers";
export const stump: Scene = {
name: "Stump",
description: "A tree stump with organic textures and natural detail",
spec: {
root: "viewer",
elements: {
viewer: {
type: "GaussianSplatViewer",
props: {
width: "100%",
height: "100vh",
backgroundColor: "#080808",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.6,
cameraPosition: [1.5, 1.5, 3],
cameraTarget: [0, 0.3, 0],
fov: 45,
},
children: [],
},
},
},
viewer: {
backgroundColor: "#080808",
controls: true,
autoRotate: true,
autoRotateSpeed: 0.6,
cameraPosition: [1.5, 1.5, 3],
cameraTarget: [0, 0.3, 0],
fov: 45,
},
splats: [
{
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/stump/stump-7k.splat",
position: [0, 0, 0],
},
],
};
-11
View File
@@ -1,11 +0,0 @@
import { nextJsConfig } from "@internal/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default [
...nextJsConfig,
{
rules: {
"react/prop-types": "off",
},
},
];
-5
View File
@@ -1,5 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;
-29
View File
@@ -1,29 +0,0 @@
{
"name": "example-gsplat",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless gsplat-demo.json-render next dev --turbopack",
"dev:local": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"gsplat": "^1.2.9",
"next": "16.1.6",
"react": "19.2.4",
"react-dom": "19.2.4"
},
"devDependencies": {
"@internal/eslint-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"eslint": "^9.39.1",
"typescript": "^5.7.2"
}
}
-13
View File
@@ -1,13 +0,0 @@
{
"extends": "../../packages/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"declaration": false,
"declarationMap": false,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
-5
View File
@@ -2,11 +2,6 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
serverExternalPackages: ["@resvg/resvg-js", "satori"],
outputFileTracingIncludes: {
"/api/image": [
"./node_modules/geist/dist/fonts/geist-sans/Geist-Regular.ttf",
],
},
};
export default nextConfig;
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -1,21 +0,0 @@
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "json-render R3F + Gaussian Splatting Example",
description:
"Gaussian splats composed with 3D primitives using @json-render/react-three-fiber",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body style={{ margin: 0, fontFamily: "system-ui, sans-serif" }}>
{children}
</body>
</html>
);
}
@@ -1,408 +0,0 @@
"use client";
import { useState, useSyncExternalStore } from "react";
import { defineCatalog } from "@json-render/core";
import { schema, defineRegistry } from "@json-render/react";
import {
threeComponentDefinitions,
threeComponents,
ThreeCanvas,
} from "@json-render/react-three-fiber";
import { scenes } from "./scenes";
const catalog = defineCatalog(schema, {
components: {
...threeComponentDefinitions,
},
actions: {},
});
const { registry } = defineRegistry(catalog, {
components: {
...threeComponents,
},
});
function highlightJson(json: string): string {
return json.replace(
/("(?:\\.|[^"\\])*")\s*(:)|("(?:\\.|[^"\\])*")|([-+]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|(\btrue\b|\bfalse\b|\bnull\b)|([{}[\]:,])/g,
(_, key, colon, str, num, lit, punct) => {
if (key) return `<span style="color:#FF4D8D">${key}</span>${colon}`;
if (str) return `<span style="color:#00CA50">${str}</span>`;
if (num) return `<span style="color:#47A8FF">${num}</span>`;
if (lit) return `<span style="color:#47A8FF">${lit}</span>`;
if (punct) return `<span style="color:#666">${punct}</span>`;
return _;
},
);
}
const MOBILE_BREAKPOINT = 768;
function subscribeToResize(cb: () => void) {
window.addEventListener("resize", cb);
return () => window.removeEventListener("resize", cb);
}
function getIsMobile() {
return typeof window !== "undefined"
? window.innerWidth < MOBILE_BREAKPOINT
: false;
}
function useIsMobile() {
return useSyncExternalStore(subscribeToResize, getIsMobile, () => false);
}
const LIST_WIDTH = 220;
const JSON_WIDTH = 380;
const HEADER_HEIGHT = 40;
const headerStyle: React.CSSProperties = {
height: HEADER_HEIGHT,
display: "flex",
alignItems: "center",
padding: "0 16px",
fontSize: 11,
fontWeight: 600,
color: "#555",
letterSpacing: "0.08em",
textTransform: "uppercase",
fontFamily: "ui-monospace, monospace",
borderBottom: "1px solid #1e1e1e",
flexShrink: 0,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
boxSizing: "border-box",
};
function DesktopLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const selected = scenes[selectedIndex]!;
return (
<div style={{ height: "100vh", display: "flex", background: "#0a0a0a" }}>
<div
style={{
width: LIST_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderRight: "1px solid #1e1e1e",
background: "#0f0f0f",
}}
>
<div style={headerStyle}>Scenes</div>
<div style={{ flex: 1, overflowY: "auto", padding: "6px 0" }}>
{scenes.map((scene, i) => (
<button
key={scene.name}
onClick={() => setSelectedIndex(i)}
style={{
display: "block",
width: "100%",
padding: "8px 16px",
fontSize: 13,
border: "none",
textAlign: "left",
background:
i === selectedIndex
? "rgba(255,255,255,0.08)"
: "transparent",
color: i === selectedIndex ? "#fff" : "#888",
fontWeight: i === selectedIndex ? 500 : 400,
cursor: "pointer",
borderLeft:
i === selectedIndex
? "2px solid #fff"
: "2px solid transparent",
fontFamily: "inherit",
}}
>
{scene.name}
</button>
))}
</div>
</div>
<div
style={{
flex: 1,
display: "flex",
flexDirection: "column",
minWidth: 0,
}}
>
<div style={headerStyle}>{selected.description}</div>
<div style={{ flex: 1, background: "#000", position: "relative" }}>
<ThreeCanvas
key={selectedIndex}
spec={selected.spec}
registry={registry}
shadows
camera={{ position: [0, 0, 5], fov: 50 }}
style={{ width: "100%", height: "100%" }}
/>
</div>
</div>
<div
style={{
width: JSON_WIDTH,
flexShrink: 0,
display: "flex",
flexDirection: "column",
borderLeft: "1px solid #1e1e1e",
background: "#0d0d0d",
}}
>
<div style={headerStyle}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(JSON.stringify(selected.spec, null, 2)),
}}
/>
</div>
</div>
);
}
function MobileLayout() {
const [selectedIndex, setSelectedIndex] = useState(0);
const [showJson, setShowJson] = useState(false);
const [showScenes, setShowScenes] = useState(false);
const selected = scenes[selectedIndex]!;
return (
<div
style={{
height: "100dvh",
display: "flex",
flexDirection: "column",
background: "#0a0a0a",
overflow: "hidden",
}}
>
<div
style={{
height: 48,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "0 12px",
borderBottom: "1px solid #1e1e1e",
background: "#0f0f0f",
flexShrink: 0,
gap: 8,
}}
>
<button
onClick={() => {
setShowScenes((v) => !v);
setShowJson(false);
}}
style={{
background: showScenes ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 12,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "inherit",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
maxWidth: "50%",
}}
>
{selected.name}
</button>
<span
style={{
flex: 1,
fontSize: 10,
color: "#555",
textAlign: "center",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontFamily: "ui-monospace, monospace",
textTransform: "uppercase",
letterSpacing: "0.06em",
}}
>
{selected.description}
</span>
<button
onClick={() => {
setShowJson((v) => !v);
setShowScenes(false);
}}
style={{
background: showJson ? "rgba(255,255,255,0.1)" : "transparent",
border: "1px solid #333",
borderRadius: 6,
color: "#ccc",
fontSize: 11,
fontWeight: 500,
padding: "6px 10px",
cursor: "pointer",
fontFamily: "ui-monospace, monospace",
whiteSpace: "nowrap",
letterSpacing: "0.04em",
}}
>
JSON
</button>
</div>
<div style={{ flex: 1, position: "relative", minHeight: 0 }}>
<ThreeCanvas
key={selectedIndex}
spec={selected.spec}
registry={registry}
shadows
camera={{ position: [0, 0, 5], fov: 50 }}
style={{ width: "100%", height: "100%" }}
/>
{showScenes && (
<>
<div
onClick={() => setShowScenes(false)}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
style={{
position: "absolute",
top: 0,
left: 0,
bottom: 0,
width: "75%",
maxWidth: 280,
background: "#0f0f0f",
borderRight: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
overflowY: "auto",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Scenes</div>
<div style={{ padding: "6px 0" }}>
{scenes.map((scene, i) => (
<button
key={scene.name}
onClick={() => {
setSelectedIndex(i);
setShowScenes(false);
}}
style={{
display: "block",
width: "100%",
padding: "10px 16px",
fontSize: 14,
border: "none",
textAlign: "left",
background:
i === selectedIndex
? "rgba(255,255,255,0.08)"
: "transparent",
color: i === selectedIndex ? "#fff" : "#888",
fontWeight: i === selectedIndex ? 500 : 400,
cursor: "pointer",
borderLeft:
i === selectedIndex
? "2px solid #fff"
: "2px solid transparent",
fontFamily: "inherit",
}}
>
{scene.name}
</button>
))}
</div>
</div>
</>
)}
{showJson && (
<>
<div
onClick={() => setShowJson(false)}
style={{
position: "absolute",
inset: 0,
background: "rgba(0,0,0,0.5)",
zIndex: 10,
}}
/>
<div
style={{
position: "absolute",
top: 0,
right: 0,
bottom: 0,
width: "85%",
maxWidth: 400,
background: "#0d0d0d",
borderLeft: "1px solid #1e1e1e",
zIndex: 11,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ ...headerStyle, height: 36 }}>Spec JSON</div>
<pre
style={{
flex: 1,
margin: 0,
padding: 14,
overflowY: "auto",
overflowX: "auto",
fontSize: 11,
lineHeight: 1.6,
fontFamily:
"ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace",
color: "#EDEDED",
tabSize: 2,
}}
dangerouslySetInnerHTML={{
__html: highlightJson(JSON.stringify(selected.spec, null, 2)),
}}
/>
</div>
</>
)}
</div>
</div>
);
}
export default function Page() {
const isMobile = useIsMobile();
return isMobile ? <MobileLayout /> : <DesktopLayout />;
}
@@ -1,3 +0,0 @@
import type { Spec } from "@json-render/core";
export type Scene = { name: string; description: string; spec: Spec };
@@ -1,166 +0,0 @@
import type { Scene } from "./_helpers";
export const floatingSplat: Scene = {
name: "Floating Splat",
description:
"A gaussian splat wrapped in Float and Spin animation wrappers with sparkle particles",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"ambient",
"key-light",
"fill-light",
"float-wrapper",
"sparkles",
"grid",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [0, 2, 6],
rotation: null,
scale: null,
fov: 45,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "studio",
background: false,
blur: 0.5,
intensity: 0.5,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#e0e0ff", intensity: 0.3 },
children: [],
},
"key-light": {
type: "DirectionalLight",
props: {
position: [5, 8, 3],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 2,
castShadow: true,
},
children: [],
},
"fill-light": {
type: "PointLight",
props: {
position: [-3, 3, -2],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 10,
distance: 15,
decay: null,
castShadow: null,
},
children: [],
},
"float-wrapper": {
type: "Float",
props: {
position: [0, 1, 0],
rotation: null,
scale: null,
speed: 1.5,
rotationIntensity: 0.2,
floatIntensity: 0.5,
enabled: true,
},
children: ["spin-wrapper"],
},
"spin-wrapper": {
type: "Spin",
props: {
position: null,
rotation: null,
scale: null,
speed: 0.2,
axis: "y",
},
children: ["splat"],
},
splat: {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/stump/stump-7k.splat",
position: [0, 0, 0],
rotation: null,
scale: [0.8, 0.8, 0.8],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
sparkles: {
type: "Sparkles",
props: {
position: [0, 1.5, 0],
rotation: null,
scale: [6, 4, 6],
count: 40,
speed: 0.08,
opacity: 0.2,
color: "#ffffff",
size: 0.4,
noise: 0.5,
},
children: [],
},
grid: {
type: "GridHelper",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
size: 20,
divisions: 20,
color: "#222222",
secondaryColor: "#111111",
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 3,
maxDistance: 15,
minPolarAngle: 0.2,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.4,
target: [0, 1, 0],
},
children: [],
},
},
},
};
@@ -1,15 +0,0 @@
import type { Scene } from "./_helpers";
import { splatShowroom } from "./splat-showroom";
import { splatWithPrimitives } from "./splat-with-primitives";
import { multiSplat } from "./multi-splat";
import { splatPostFx } from "./splat-postfx";
import { floatingSplat } from "./floating-splat";
export type { Scene };
export const scenes: Scene[] = [
splatShowroom,
splatWithPrimitives,
multiSplat,
splatPostFx,
floatingSplat,
];
@@ -1,146 +0,0 @@
import type { Scene } from "./_helpers";
export const multiSplat: Scene = {
name: "Multi-Splat Gallery",
description: "Multiple gaussian splats arranged in a gallery with spotlights",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"ambient",
"spot-left",
"spot-right",
"splat-left",
"splat-right",
"grid",
"fog",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [0, 3, 8],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#e0e0ff", intensity: 0.2 },
children: [],
},
"spot-left": {
type: "SpotLight",
props: {
position: [-3, 6, 2],
rotation: null,
scale: null,
color: "#ffddaa",
intensity: 60,
distance: 15,
decay: null,
angle: 0.5,
penumbra: 0.8,
castShadow: true,
},
children: [],
},
"spot-right": {
type: "SpotLight",
props: {
position: [3, 6, 2],
rotation: null,
scale: null,
color: "#aaddff",
intensity: 60,
distance: 15,
decay: null,
angle: 0.5,
penumbra: 0.8,
castShadow: true,
},
children: [],
},
"splat-left": {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [-3, 0, 0],
rotation: null,
scale: [0.8, 0.8, 0.8],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
"splat-right": {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/garden/garden-7k.splat",
position: [3, 0, 0],
rotation: null,
scale: [0.8, 0.8, 0.8],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
grid: {
type: "GridHelper",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
size: 30,
divisions: 30,
color: "#222222",
secondaryColor: "#111111",
},
children: [],
},
fog: {
type: "Fog",
props: {
color: "#0a0a0a",
near: 8,
far: 25,
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 3,
maxDistance: 18,
minPolarAngle: 0.3,
maxPolarAngle: 1.4,
autoRotate: true,
autoRotateSpeed: 0.3,
target: [0, 0.5, 0],
},
children: [],
},
},
},
};
@@ -1,134 +0,0 @@
import type { Scene } from "./_helpers";
export const splatPostFx: Scene = {
name: "Splat + Post-Processing",
description:
"A gaussian splat with bloom and vignette post-processing effects",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"ambient",
"key-light",
"splat",
"grid",
"fx",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [3, 2, 5],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "night",
background: false,
blur: 0.8,
intensity: 0.4,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ccccff", intensity: 0.2 },
children: [],
},
"key-light": {
type: "PointLight",
props: {
position: [3, 5, 3],
rotation: null,
scale: null,
color: "#ffeecc",
intensity: 30,
distance: 20,
decay: null,
castShadow: null,
},
children: [],
},
splat: {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [0, 0, 0],
rotation: null,
scale: [1, 1, 1],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
grid: {
type: "GridHelper",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
size: 20,
divisions: 20,
color: "#222222",
secondaryColor: "#111111",
},
children: [],
},
fx: {
type: "EffectComposer",
props: { enabled: true, multisampling: 8 },
children: ["bloom", "vignette"],
},
bloom: {
type: "Bloom",
props: {
intensity: 0.8,
luminanceThreshold: 0.2,
luminanceSmoothing: 0.5,
mipmapBlur: true,
},
children: [],
},
vignette: {
type: "Vignette",
props: { offset: 0.25, darkness: 0.7 },
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 2,
maxDistance: 12,
minPolarAngle: 0.3,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.5,
target: [0, 0.5, 0],
},
children: [],
},
},
},
};
@@ -1,126 +0,0 @@
import type { Scene } from "./_helpers";
export const splatShowroom: Scene = {
name: "Splat Showroom",
description:
"A gaussian splat on a pedestal with studio lighting and orbit controls",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"env",
"ambient",
"key-light",
"fill-light",
"splat",
"grid",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [3, 2, 5],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
env: {
type: "Environment",
props: {
preset: "studio",
background: false,
blur: 0.5,
intensity: 0.6,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#e8e0f0", intensity: 0.3 },
children: [],
},
"key-light": {
type: "DirectionalLight",
props: {
position: [5, 8, 3],
rotation: null,
scale: null,
color: "#ffffff",
intensity: 2,
castShadow: true,
},
children: [],
},
"fill-light": {
type: "PointLight",
props: {
position: [-3, 4, -2],
rotation: null,
scale: null,
color: "#aaccff",
intensity: 15,
distance: 20,
decay: null,
castShadow: null,
},
children: [],
},
splat: {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [0, 0, 0],
rotation: null,
scale: [1, 1, 1],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
grid: {
type: "GridHelper",
props: {
position: [0, -0.5, 0],
rotation: null,
scale: null,
size: 20,
divisions: 20,
color: "#333333",
secondaryColor: "#1a1a1a",
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 2,
maxDistance: 15,
minPolarAngle: 0.2,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.5,
target: [0, 0.5, 0],
},
children: [],
},
},
},
};
@@ -1,157 +0,0 @@
import type { Scene } from "./_helpers";
export const splatWithPrimitives: Scene = {
name: "Splat + Primitives",
description:
"A gaussian splat scene composed with standard 3D primitives and lights",
spec: {
root: "scene",
elements: {
scene: {
type: "Group",
props: { position: null, rotation: null, scale: null },
children: [
"camera",
"ambient",
"sun",
"splat",
"logo",
"label",
"grid",
"controls",
],
},
camera: {
type: "PerspectiveCamera",
props: {
position: [4, 3, 6],
rotation: null,
scale: null,
fov: 50,
near: null,
far: null,
makeDefault: true,
},
children: [],
},
ambient: {
type: "AmbientLight",
props: { color: "#ffffff", intensity: 0.4 },
children: [],
},
sun: {
type: "DirectionalLight",
props: {
position: [8, 10, 5],
rotation: null,
scale: null,
color: "#fff5e0",
intensity: 2.5,
castShadow: true,
},
children: [],
},
splat: {
type: "GaussianSplat",
props: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/garden/garden-7k.splat",
position: [0, 0, 0],
rotation: null,
scale: [1, 1, 1],
castShadow: null,
receiveShadow: null,
alphaHash: null,
toneMapped: null,
visible: true,
},
children: [],
},
logo: {
type: "Cone",
props: {
position: [-0.4, -0.25, 1],
rotation: [-1.5708, 0, 0],
scale: null,
radius: 0.1,
height: 0.06,
radialSegments: 3,
castShadow: true,
receiveShadow: null,
material: {
color: "#ffffff",
metalness: 0.3,
roughness: 0.4,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
},
children: [],
},
grid: {
type: "GridHelper",
props: {
position: [0, -0.49, 0],
rotation: null,
scale: null,
size: 30,
divisions: 30,
color: "#333333",
secondaryColor: "#1a1a1a",
},
children: [],
},
label: {
type: "ExtrudedText",
props: {
position: [-0.3, -0.28, 1],
rotation: null,
scale: null,
text: "vercel-labs/json-render",
font: null,
size: 0.1,
depth: 0.02,
curveSegments: null,
bevelEnabled: false,
bevelThickness: null,
bevelSize: null,
bevelSegments: null,
centered: false,
castShadow: true,
receiveShadow: null,
material: {
color: "#ffffff",
metalness: 1,
roughness: 0.4,
emissive: null,
emissiveIntensity: null,
opacity: null,
transparent: null,
wireframe: null,
},
},
children: [],
},
controls: {
type: "OrbitControls",
props: {
enableDamping: true,
dampingFactor: null,
enableZoom: null,
enablePan: null,
enableRotate: null,
minDistance: 2,
maxDistance: 20,
minPolarAngle: 0.2,
maxPolarAngle: 1.5,
autoRotate: true,
autoRotateSpeed: 0.3,
target: [0.3, -0.4, 0.4],
},
children: [],
},
},
},
};
@@ -1,12 +0,0 @@
import { nextJsConfig } from "@internal/eslint-config/next-js";
/** @type {import("eslint").Linter.Config[]} */
export default [
...nextJsConfig,
{
rules: {
"react/prop-types": "off",
"react/no-unknown-property": "off",
},
},
];
@@ -1,12 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
transpilePackages: [
"@json-render/core",
"@json-render/react",
"@json-render/react-three-fiber",
"three",
],
};
export default nextConfig;
@@ -1,36 +0,0 @@
{
"name": "example-react-three-fiber-gsplat",
"version": "0.1.0",
"type": "module",
"private": true,
"scripts": {
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "portless r3f-gsplat-demo.json-render next dev --turbopack",
"dev:local": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "eslint --max-warnings 0",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/react": "workspace:*",
"@json-render/react-three-fiber": "workspace:*",
"@react-three/drei": "^10.7.7",
"@react-three/fiber": "^9.5.0",
"next": "16.1.6",
"react": "19.2.4",
"react-dom": "19.2.4",
"three": "^0.183.2",
"zod": "4.3.6"
},
"devDependencies": {
"@internal/eslint-config": "workspace:*",
"@types/node": "^22.10.0",
"@types/react": "19.2.3",
"@types/react-dom": "19.2.3",
"@types/three": "^0.183.1",
"eslint": "^9.39.1",
"typescript": "^5.7.2"
}
}
@@ -1,13 +0,0 @@
{
"extends": "../../packages/typescript-config/nextjs.json",
"compilerOptions": {
"plugins": [{ "name": "next" }],
"declaration": false,
"declarationMap": false,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+6
View File
@@ -0,0 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+5 -4
View File
@@ -13,7 +13,7 @@
"scripts": {
"build": "turbo run build",
"predev": "command -v portless >/dev/null 2>&1 || (echo '\\nportless is required but not installed. Run: npm i -g portless\\nSee: https://github.com/vercel-labs/portless\\n' && exit 1)",
"dev": "turbo run dev --concurrency 50",
"dev": "turbo run dev --concurrency 20",
"lint": "turbo run lint",
"format": "prettier --write \"**/*.{ts,tsx}\"",
"type-check": "turbo run check-types",
@@ -23,12 +23,13 @@
"test:coverage": "vitest run --coverage",
"test:e2e": "pnpm --filter e2e-tests test",
"prepare": "husky",
"version:sync": "node scripts/sync-version.js",
"version:check": "node scripts/check-version-sync.js",
"ci:publish": "pnpm run build && pnpm -r publish --no-git-checks --filter '@json-render/*'",
"changeset": "changeset",
"ci:version": "changeset version && pnpm install --no-frozen-lockfile",
"ci:publish": "pnpm run build && changeset publish",
"generate:og": "npx tsx scripts/generate-og-images.mts"
},
"devDependencies": {
"@changesets/cli": "2.29.8",
"@resvg/resvg-js": "2.6.2",
"@solidjs/testing-library": "^0.8.10",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/codegen",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Utilities for generating code from json-render UI trees",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/core",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
"keywords": [
+22 -23
View File
@@ -5,6 +5,7 @@ import {
resolveBindings,
resolveActionParam,
_resetWarnedComputedFns,
_resetWarnedTemplatePaths,
} from "./props";
import type { PropResolutionContext } from "./props";
@@ -634,32 +635,30 @@ describe("$template expressions", () => {
);
});
it("resolves bare names against state model when not in repeat", () => {
it("warns when path does not start with /", () => {
_resetWarnedTemplatePaths();
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const ctx: PropResolutionContext = { stateModel: { name: "Bob" } };
const result = resolvePropValue({ $template: "Hi ${name}!" }, ctx);
expect(result).toBe("Hi Bob!");
});
it("resolves bare names against repeat item first", () => {
const ctx: PropResolutionContext = {
stateModel: { name: "Global" },
repeatItem: { name: "Alice", action: "signed up" },
repeatIndex: 0,
};
const result = resolvePropValue({ $template: "${name} ${action}" }, ctx);
expect(result).toBe("Alice signed up");
});
it("falls back to state model for bare names not in repeat item", () => {
const ctx: PropResolutionContext = {
stateModel: { fallback: "ok" },
repeatItem: { name: "Alice" },
repeatIndex: 0,
};
const result = resolvePropValue(
{ $template: "${name} - ${fallback}" },
ctx,
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('$template path "name"'),
);
expect(result).toBe("Alice - ok");
warnSpy.mockRestore();
_resetWarnedTemplatePaths();
});
it("deduplicates warnings for the same $template path", () => {
_resetWarnedTemplatePaths();
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const ctx: PropResolutionContext = { stateModel: { name: "Bob" } };
resolvePropValue({ $template: "Hi ${name}!" }, ctx);
resolvePropValue({ $template: "Hi ${name}!" }, ctx);
const calls = warnSpy.mock.calls.filter((c) =>
String(c[0]).includes('$template path "name"'),
);
expect(calls).toHaveLength(1);
warnSpy.mockRestore();
_resetWarnedTemplatePaths();
});
});
+24 -18
View File
@@ -24,10 +24,8 @@ import { evaluateVisibility, type VisibilityContext } from "./visibility";
* - `{ $cond, $then, $else }` conditionally picks a value
* - `{ $computed: string, args?: Record<string, PropExpression> }` calls a
* registered function with resolved args and returns the result
* - `{ $template: string }` interpolates `${/path}` and `${field}`
* references. Absolute paths (`${/path}`) resolve against the state
* model. Bare names (`${field}`) resolve against the current repeat
* item first, then fall back to the state model at `/<field>`.
* - `{ $template: string }` interpolates `${/path}` references in the
* string with values from the state model
* - Any other value is a literal (passthrough)
*/
export type PropExpression<T = unknown> =
@@ -158,6 +156,15 @@ export function _resetWarnedComputedFns(): void {
warnedComputedFns.clear();
}
// Same deduplication pattern for $template paths that don't start with "/".
const WARNED_TEMPLATE_MAX = 100;
const warnedTemplatePaths = new Set<string>();
/** @internal Test-only: clear the deduplication set for $template warnings. */
export function _resetWarnedTemplatePaths(): void {
warnedTemplatePaths.clear();
}
// =============================================================================
// Prop Expression Resolution
// =============================================================================
@@ -262,25 +269,24 @@ export function resolvePropValue(
return fn(resolvedArgs);
}
// $template: interpolate ${/path} and ${field} references.
// Absolute paths (starting with "/") resolve against the state model.
// Bare names resolve against the current repeat item first, then fall
// back to the state model at "/<name>".
// $template: interpolate ${/path} references with state values
if (isTemplateExpression(value)) {
return value.$template.replace(
/\$\{([^}]+)\}/g,
(_match, rawPath: string) => {
if (rawPath.startsWith("/")) {
const resolved = getByPath(ctx.stateModel, rawPath);
return resolved != null ? String(resolved) : "";
let path = rawPath;
if (!path.startsWith("/")) {
if (!warnedTemplatePaths.has(path)) {
if (warnedTemplatePaths.size < WARNED_TEMPLATE_MAX) {
warnedTemplatePaths.add(path);
}
console.warn(
`$template path "${path}" should be a JSON Pointer starting with "/". Automatically resolving as "/${path}".`,
);
}
path = "/" + path;
}
// Bare name: try repeat item first
if (ctx.repeatItem !== undefined) {
const fromItem = getByPath(ctx.repeatItem, rawPath);
if (fromItem != null) return String(fromItem);
}
// Fall back to state model
const resolved = getByPath(ctx.stateModel, "/" + rawPath);
const resolved = getByPath(ctx.stateModel, path);
return resolved != null ? String(resolved) : "";
},
);
-64
View File
@@ -189,70 +189,6 @@ describe("catalog.prompt", () => {
expect(prompt).toContain("users: Array<{ name: string, age: number }>");
});
it("formats z.literal() as quoted value", () => {
const catalog = defineCatalog(testSchema, {
components: {
Config: {
props: z.object({
version: z.literal("3.0"),
count: z.literal(42),
}),
description: "",
slots: [],
},
},
actions: {},
});
const prompt = catalog.prompt();
expect(prompt).toContain('version: "3.0"');
expect(prompt).toContain("count: 42");
});
it("formats z.default() by unwrapping to inner type", () => {
const catalog = defineCatalog(testSchema, {
components: {
Form: {
props: z.object({
enabled: z.boolean().default(false),
count: z.number().default(0),
tags: z.array(z.string()).default([]),
}),
description: "",
slots: [],
},
},
actions: {},
});
const prompt = catalog.prompt();
expect(prompt).toContain("enabled: boolean");
expect(prompt).toContain("count: number");
expect(prompt).toContain("tags: Array<string>");
});
it("formats z.record() as Record<K, V>", () => {
const catalog = defineCatalog(testSchema, {
components: {
Store: {
props: z.object({
simple: z.record(z.string(), z.number()),
nested: z.record(
z.string(),
z.object({ id: z.string(), score: z.number() }),
),
}),
description: "",
slots: [],
},
},
actions: {},
});
const prompt = catalog.prompt();
expect(prompt).toContain("simple: Record<string, number>");
expect(prompt).toContain(
"nested: Record<string, { id: string, score: number }>",
);
});
it("includes AVAILABLE ACTIONS when present", () => {
const catalog = defineCatalog(testSchema, {
components: {
+4 -22
View File
@@ -943,10 +943,10 @@ Note: state patches appear right after the elements that use them, so the UI fil
);
lines.push("");
lines.push(
'4. Template: `{ "$template": "Hello, ${/name}!" }` - interpolates references in the string. Absolute paths like `${/path}` resolve against the state model. Bare names like `${field}` resolve against the current repeat item first, then fall back to the state model at `/<field>`.',
'4. Template: `{ "$template": "Hello, ${/name}!" }` - interpolates `${/path}` references in the string with values from the state model.',
);
lines.push(
' Example: `"label": { "$template": "Items: ${/cart/count} | Total: ${/cart/total}" }` renders "Items: 3 | Total: 42.00" when /cart/count is 3 and /cart/total is 42.00. Inside a repeat, `{ "$template": "${name} - ${email}" }` reads name and email from each item.',
' Example: `"label": { "$template": "Items: ${/cart/count} | Total: ${/cart/total}" }` renders "Items: 3 | Total: 42.00" when /cart/count is 3 and /cart/total is 42.00.',
);
lines.push("");
@@ -1283,12 +1283,8 @@ function formatZodType(schema: z.ZodType): string {
case "boolean":
return "boolean";
case "ZodLiteral":
case "literal": {
// Zod 4 uses def.values (array), Zod 3 uses def.value
const litValues = def.values as unknown[] | undefined;
const litValue = litValues?.[0] ?? def.value;
return JSON.stringify(litValue);
}
case "literal":
return JSON.stringify(def.value);
case "ZodEnum":
case "enum": {
// Zod 3 uses values array, Zod 4 uses entries object
@@ -1349,20 +1345,6 @@ function formatZodType(schema: z.ZodType): string {
? options.map((opt) => formatZodType(opt)).join(" | ")
: "unknown";
}
case "ZodRecord":
case "record": {
const keyType = (def.keyType as z.ZodType) ?? undefined;
const valueType =
(def.valueType as z.ZodType) ?? (def.element as z.ZodType) ?? undefined;
const keyStr = keyType ? formatZodType(keyType) : "string";
const valueStr = valueType ? formatZodType(valueType) : "unknown";
return `Record<${keyStr}, ${valueStr}>`;
}
case "ZodDefault":
case "default": {
const inner = (def.innerType as z.ZodType) ?? (def.wrapped as z.ZodType);
return inner ? formatZodType(inner) : "unknown";
}
default:
return "unknown";
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/image",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/ink",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/jotai",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Jotai adapter for json-render StateStore",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/mcp",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "MCP Apps integration for @json-render/core. Serve json-render UIs as interactive MCP Apps in Claude, ChatGPT, Cursor, and VS Code.",
"keywords": [
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/next",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Next.js renderer for @json-render/core. JSON becomes full Next.js applications with routes, layouts, metadata, and SSR.",
"keywords": [
@@ -48,7 +48,7 @@
"dist"
],
"scripts": {
"build": "rm -rf dist && tsup",
"build": "tsup",
"dev": "tsup --watch",
"check-types": "tsc --noEmit",
"typecheck": "tsc --noEmit"
+1
View File
@@ -20,6 +20,7 @@ export default defineConfig([
dts: true,
sourcemap: true,
splitting: true,
clean: true,
banner: { js: '"use client";' },
external: sharedExternal,
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-email",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-native",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-pdf",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
"keywords": [
+1 -1
View File
@@ -66,7 +66,7 @@ const { registry } = defineRegistry(catalog, {
**Environment:** Environment, Fog, GridHelper
**Text:** Text3D, ExtrudedText
**Text:** Text3D
**Camera/Controls:** PerspectiveCamera, OrbitControls
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react-three-fiber",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
"keywords": [
-47
View File
@@ -319,32 +319,6 @@ export const threeComponentDefinitions = {
},
},
ExtrudedText: {
props: z.object({
...transformProps,
...shadowProps,
material: materialSchema.nullable(),
text: z.string(),
font: z.string().nullable(),
size: z.number().nullable(),
depth: z.number().nullable(),
curveSegments: z.number().nullable(),
bevelEnabled: z.boolean().nullable(),
bevelThickness: z.number().nullable(),
bevelSize: z.number().nullable(),
bevelSegments: z.number().nullable(),
centered: z.boolean().nullable(),
}),
description:
"Extruded 3D text with depth. Uses a typeface JSON font for geometry-based rendering with bevel support.",
example: {
text: "Hello",
size: 1,
depth: 0.2,
position: [0, 2, 0],
},
},
// ===========================================================================
// Effects / Atmosphere
// ===========================================================================
@@ -842,27 +816,6 @@ export const threeComponentDefinitions = {
"Orbit camera controls. Allows the user to rotate, zoom, and pan around the scene.",
example: { enableDamping: true, autoRotate: false },
},
// ===========================================================================
// Gaussian Splatting
// ===========================================================================
GaussianSplat: {
props: z.object({
src: z.string(),
...transformProps,
...shadowProps,
alphaHash: z.boolean().nullable(),
toneMapped: z.boolean().nullable(),
visible: z.boolean().nullable(),
}),
description:
"Loads and renders a .splat or .ply gaussian splat file inside an R3F scene. Composable with all other 3D components (lights, models, primitives). Uses drei's Splat loader.",
example: {
src: "https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k.splat",
position: [0, 0, 0],
},
},
};
// =============================================================================
@@ -6,10 +6,7 @@ import {
OrbitControls as DreiOrbitControls,
PerspectiveCamera as DreiPerspectiveCamera,
Text as DreiText,
Text3D as DreiText3D,
Center as DreiCenter,
Gltf,
Splat as DreiSplat,
Sparkles as DreiSparkles,
Stars as DreiStars,
Sky as DreiSky,
@@ -353,41 +350,6 @@ export const threeComponents = {
</DreiText>
),
ExtrudedText: ({ props }: BaseComponentProps<ThreeProps<"ExtrudedText">>) => {
const font =
props.font ??
"https://cdn.jsdelivr.net/npm/three/examples/fonts/helvetiker_regular.typeface.json";
const inner = (
<DreiText3D
font={font}
size={props.size ?? 1}
height={props.depth ?? 0.2}
curveSegments={props.curveSegments ?? 12}
bevelEnabled={props.bevelEnabled ?? false}
bevelThickness={props.bevelThickness ?? 0.02}
bevelSize={props.bevelSize ?? 0.02}
bevelSegments={props.bevelSegments ?? 3}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
>
{props.text}
<MaterialComponent material={props.material} />
</DreiText3D>
);
return (
<Suspense fallback={null}>
<group
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
>
{(props.centered ?? true) ? <DreiCenter>{inner}</DreiCenter> : inner}
</group>
</Suspense>
);
},
// ── Effects / Atmosphere ──────────────────────────────────────────────
Sparkles: ({ props }: BaseComponentProps<ThreeProps<"Sparkles">>) => (
@@ -944,24 +906,4 @@ export const threeComponents = {
target={props.target ?? undefined}
/>
),
// ── Gaussian Splatting ──────────────────────────────────────────────
GaussianSplat: ({
props,
}: BaseComponentProps<ThreeProps<"GaussianSplat">>) => (
<Suspense fallback={null}>
<DreiSplat
src={props.src}
position={pos(props.position)}
rotation={rot(props.rotation)}
scale={scl(props.scale)}
castShadow={props.castShadow ?? false}
receiveShadow={props.receiveShadow ?? false}
alphaHash={props.alphaHash ?? undefined}
toneMapped={props.toneMapped ?? undefined}
visible={props.visible ?? true}
/>
</Suspense>
),
};
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/react",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "React renderer for @json-render/core. JSON becomes React components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/redux",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Redux adapter for json-render StateStore",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/remotion",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
"keywords": [
+3 -5
View File
@@ -1,5 +1,4 @@
import { defineSchema, type PromptContext } from "@json-render/core";
import type { z } from "zod";
/**
* Prompt template for Remotion timeline generation
@@ -7,7 +6,7 @@ import type { z } from "zod";
* Uses JSONL patch format (same as React) but builds up a timeline spec structure.
*/
function remotionPromptTemplate(context: PromptContext): string {
const { catalog, options, formatZodType } = context;
const { catalog, options } = context;
const { system = "You are a video timeline generator.", customRules = [] } =
options;
@@ -38,7 +37,7 @@ function remotionPromptTemplate(context: PromptContext): string {
const catalogData = catalog as {
components?: Record<
string,
{ description?: string; defaultDuration?: number; props?: z.ZodType }
{ description?: string; defaultDuration?: number }
>;
transitions?: Record<string, { description?: string }>;
effects?: Record<string, { description?: string }>;
@@ -53,9 +52,8 @@ function remotionPromptTemplate(context: PromptContext): string {
const duration = def.defaultDuration
? ` [default: ${def.defaultDuration} frames]`
: "";
const propsStr = def.props ? ` ${formatZodType(def.props)}` : "";
lines.push(
`- ${name}:${propsStr} ${def.description || "No description"}${duration}`,
`- ${name}: ${def.description || "No description"}${duration}`,
);
}
lines.push("");
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/shadcn-svelte",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "shadcn-svelte component library for @json-render/svelte. JSON becomes beautiful Tailwind-styled Svelte components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/shadcn",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/solid",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/svelte",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/vue",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Vue renderer for @json-render/core. JSON becomes Vue components.",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/xstate",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "XState Store adapter for json-render StateStore",
"keywords": [
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/yaml",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.",
"keywords": [
+2 -20
View File
@@ -56,12 +56,8 @@ function formatZodType(schema: ZodLike): string {
case "boolean":
return "boolean";
case "ZodLiteral":
case "literal": {
// Zod 4 uses def.values (array), Zod 3 uses def.value
const litValues = def.values as unknown[] | undefined;
const litValue = litValues?.[0] ?? def.value;
return JSON.stringify(litValue);
}
case "literal":
return JSON.stringify(def.value);
case "ZodEnum":
case "enum": {
let values: string[];
@@ -119,20 +115,6 @@ function formatZodType(schema: ZodLike): string {
? options.map((opt) => formatZodType(opt)).join(" | ")
: "unknown";
}
case "ZodRecord":
case "record": {
const keyType = (def.keyType as ZodLike) ?? undefined;
const valueType =
(def.valueType as ZodLike) ?? (def.element as ZodLike) ?? undefined;
const keyStr = keyType ? formatZodType(keyType) : "string";
const valueStr = valueType ? formatZodType(valueType) : "unknown";
return `Record<${keyStr}, ${valueStr}>`;
}
case "ZodDefault":
case "default": {
const inner = (def.innerType as ZodLike) ?? (def.wrapped as ZodLike);
return inner ? formatZodType(inner) : "unknown";
}
default:
return "unknown";
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@json-render/zustand",
"version": "0.17.0",
"version": "0.16.0",
"license": "Apache-2.0",
"description": "Zustand adapter for json-render StateStore",
"keywords": [
+414 -97
View File
@@ -8,6 +8,9 @@ importers:
.:
devDependencies:
'@changesets/cli':
specifier: 2.29.8
version: 2.29.8(@types/node@22.19.6)
'@resvg/resvg-js':
specifier: 2.6.2
version: 2.6.2
@@ -565,40 +568,6 @@ importers:
specifier: ^5.7.2
version: 5.9.3
examples/gsplat:
dependencies:
gsplat:
specifier: ^1.2.9
version: 1.2.9
next:
specifier: 16.1.6
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
devDependencies:
'@internal/eslint-config':
specifier: workspace:*
version: link:../../packages/eslint-config
'@types/node':
specifier: ^22.10.0
version: 22.19.6
'@types/react':
specifier: 19.2.3
version: 19.2.3
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.3)
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@2.6.1)
typescript:
specifier: ^5.7.2
version: 5.9.3
examples/image:
dependencies:
'@json-render/core':
@@ -1208,61 +1177,6 @@ importers:
specifier: ^5.7.2
version: 5.9.3
examples/react-three-fiber-gsplat:
dependencies:
'@json-render/core':
specifier: workspace:*
version: link:../../packages/core
'@json-render/react':
specifier: workspace:*
version: link:../../packages/react
'@json-render/react-three-fiber':
specifier: workspace:*
version: link:../../packages/react-three-fiber
'@react-three/drei':
specifier: ^10.7.7
version: 10.7.7(@react-three/fiber@9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2))(@types/react@19.2.3)(@types/three@0.183.1)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(three@0.183.2)
'@react-three/fiber':
specifier: ^9.5.0
version: 9.5.0(@types/react@19.2.3)(expo-asset@12.0.12(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4))(expo-file-system@19.0.21(expo@54.0.33)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)))(expo@54.0.33)(immer@11.1.4)(react-dom@19.2.4(react@19.2.4))(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)(three@0.183.2)
next:
specifier: 16.1.6
version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
react:
specifier: 19.2.4
version: 19.2.4
react-dom:
specifier: 19.2.4
version: 19.2.4(react@19.2.4)
three:
specifier: ^0.183.2
version: 0.183.2
zod:
specifier: 4.3.6
version: 4.3.6
devDependencies:
'@internal/eslint-config':
specifier: workspace:*
version: link:../../packages/eslint-config
'@types/node':
specifier: ^22.10.0
version: 22.19.6
'@types/react':
specifier: 19.2.3
version: 19.2.3
'@types/react-dom':
specifier: 19.2.3
version: 19.2.3(@types/react@19.2.3)
'@types/three':
specifier: ^0.183.1
version: 0.183.1
eslint:
specifier: ^9.39.1
version: 9.39.2(jiti@2.6.1)
typescript:
specifier: ^5.7.2
version: 5.9.3
examples/remotion:
dependencies:
'@ai-sdk/gateway':
@@ -3107,6 +3021,61 @@ packages:
'@borewit/text-codec@0.2.2':
resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==}
'@changesets/apply-release-plan@7.0.14':
resolution: {integrity: sha512-ddBvf9PHdy2YY0OUiEl3TV78mH9sckndJR14QAt87KLEbIov81XO0q0QAmvooBxXlqRRP8I9B7XOzZwQG7JkWA==}
'@changesets/assemble-release-plan@6.0.9':
resolution: {integrity: sha512-tPgeeqCHIwNo8sypKlS3gOPmsS3wP0zHt67JDuL20P4QcXiw/O4Hl7oXiuLnP9yg+rXLQ2sScdV1Kkzde61iSQ==}
'@changesets/changelog-git@0.2.1':
resolution: {integrity: sha512-x/xEleCFLH28c3bQeQIyeZf8lFXyDFVn1SgcBiR2Tw/r4IAWlk1fzxCEZ6NxQAjF2Nwtczoen3OA2qR+UawQ8Q==}
'@changesets/cli@2.29.8':
resolution: {integrity: sha512-1weuGZpP63YWUYjay/E84qqwcnt5yJMM0tep10Up7Q5cS/DGe2IZ0Uj3HNMxGhCINZuR7aO9WBMdKnPit5ZDPA==}
hasBin: true
'@changesets/config@3.1.2':
resolution: {integrity: sha512-CYiRhA4bWKemdYi/uwImjPxqWNpqGPNbEBdX1BdONALFIDK7MCUj6FPkzD+z9gJcvDFUQJn9aDVf4UG7OT6Kog==}
'@changesets/errors@0.2.0':
resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==}
'@changesets/get-dependents-graph@2.1.3':
resolution: {integrity: sha512-gphr+v0mv2I3Oxt19VdWRRUxq3sseyUpX9DaHpTUmLj92Y10AGy+XOtV+kbM6L/fDcpx7/ISDFK6T8A/P3lOdQ==}
'@changesets/get-release-plan@4.0.14':
resolution: {integrity: sha512-yjZMHpUHgl4Xl5gRlolVuxDkm4HgSJqT93Ri1Uz8kGrQb+5iJ8dkXJ20M2j/Y4iV5QzS2c5SeTxVSKX+2eMI0g==}
'@changesets/get-version-range-type@0.4.0':
resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==}
'@changesets/git@3.0.4':
resolution: {integrity: sha512-BXANzRFkX+XcC1q/d27NKvlJ1yf7PSAgi8JG6dt8EfbHFHi4neau7mufcSca5zRhwOL8j9s6EqsxmT+s+/E6Sw==}
'@changesets/logger@0.1.1':
resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==}
'@changesets/parse@0.4.2':
resolution: {integrity: sha512-Uo5MC5mfg4OM0jU3up66fmSn6/NE9INK+8/Vn/7sMVcdWg46zfbvvUSjD9EMonVqPi9fbrJH9SXHn48Tr1f2yA==}
'@changesets/pre@2.0.2':
resolution: {integrity: sha512-HaL/gEyFVvkf9KFg6484wR9s0qjAXlZ8qWPDkTyKF6+zqjBe/I2mygg3MbpZ++hdi0ToqNUF8cjj7fBy0dg8Ug==}
'@changesets/read@0.6.6':
resolution: {integrity: sha512-P5QaN9hJSQQKJShzzpBT13FzOSPyHbqdoIBUd2DJdgvnECCyO6LmAOWSV+O8se2TaZJVwSXjL+v9yhb+a9JeJg==}
'@changesets/should-skip-package@0.1.2':
resolution: {integrity: sha512-qAK/WrqWLNCP22UDdBTMPH5f41elVDlsNyat180A33dWxuUDyNpg6fPi/FyTZwRriVjg0L8gnjJn2F9XAoF0qw==}
'@changesets/types@4.1.0':
resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==}
'@changesets/types@6.1.0':
resolution: {integrity: sha512-rKQcJ+o1nKNgeoYRHKOS07tAMNd3YSN0uHaJOZYjBAgxfV7TUE7JE+z4BzZdQwb5hKaYbayKN5KrYV7ODb2rAA==}
'@changesets/write@0.4.0':
resolution: {integrity: sha512-CdTLvIOPiCNuH71pyDu3rA+Q0n65cmAbXnwWH84rKGiFumFzkmHNT8KHTMEchcxN+Kl8I54xGUhJ7l3E7X396Q==}
'@csstools/color-helpers@5.1.0':
resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==}
engines: {node: '>=18'}
@@ -4189,6 +4158,15 @@ packages:
'@types/node':
optional: true
'@inquirer/external-editor@1.0.3':
resolution: {integrity: sha512-RWbSrDiYmO4LbejWY7ttpxczuwQyZLBUyygsA9Nsv95hpzUWwnNTVQmAq3xuh7vNwCp07UTmE5i11XAEExx4RA==}
engines: {node: '>=18'}
peerDependencies:
'@types/node': '>=18'
peerDependenciesMeta:
'@types/node':
optional: true
'@inquirer/figures@1.0.15':
resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==}
engines: {node: '>=18'}
@@ -4359,6 +4337,12 @@ packages:
peerDependencies:
svelte: ^5
'@manypkg/find-root@1.1.0':
resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==}
'@manypkg/get-packages@1.1.3':
resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==}
'@mdx-js/loader@3.1.1':
resolution: {integrity: sha512-0TTacJyZ9mDmY+VefuthVshaNIyCGZHJG2fMnGaDttCt8HmjUF7SizlHJpaCDoGnN635nK1wpzfpx/Xx5S4WnQ==}
peerDependencies:
@@ -6737,6 +6721,9 @@ packages:
'@types/ms@2.1.0':
resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==}
'@types/node@12.20.55':
resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==}
'@types/node@22.19.6':
resolution: {integrity: sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==}
@@ -7344,6 +7331,10 @@ packages:
anser@1.4.10:
resolution: {integrity: sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==}
ansi-colors@4.1.3:
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
engines: {node: '>=6'}
ansi-escapes@4.3.2:
resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==}
engines: {node: '>=8'}
@@ -7627,6 +7618,10 @@ packages:
resolution: {integrity: sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==}
engines: {node: '>=12.0.0'}
better-path-resolve@1.0.0:
resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==}
engines: {node: '>=4'}
bidi-js@1.0.3:
resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==}
@@ -7805,6 +7800,9 @@ packages:
character-reference-invalid@2.0.1:
resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==}
chardet@2.1.1:
resolution: {integrity: sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==}
chokidar@4.0.3:
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
engines: {node: '>= 14.16.0'}
@@ -8298,6 +8296,10 @@ packages:
detect-gpu@5.0.70:
resolution: {integrity: sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==}
detect-indent@6.1.0:
resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==}
engines: {node: '>=8'}
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
@@ -8567,6 +8569,10 @@ packages:
resolution: {integrity: sha512-phv3E1Xl4tQOShqSte26C7Fl84EwUdZsyOuSSk9qtAGyyQs2s3jJzComh+Abf4g187lUUAvH+H26omrqia2aGg==}
engines: {node: '>=10.13.0'}
enquirer@2.4.1:
resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==}
engines: {node: '>=8.6'}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@@ -9029,6 +9035,9 @@ packages:
extend@3.0.2:
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
extendable-error@0.1.7:
resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==}
extract-zip@2.0.1:
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
engines: {node: '>= 10.17.0'}
@@ -9203,6 +9212,14 @@ packages:
resolution: {integrity: sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==}
engines: {node: '>=14.14'}
fs-extra@7.0.1:
resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==}
engines: {node: '>=6 <7 || >=8'}
fs-extra@8.1.0:
resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==}
engines: {node: '>=6 <7 || >=8'}
fs-monkey@1.0.3:
resolution: {integrity: sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q==}
@@ -9373,9 +9390,6 @@ packages:
resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==}
engines: {node: '>=6.0'}
gsplat@1.2.9:
resolution: {integrity: sha512-zfpaL9TEQD4oHNesL77KYeRgWvIy2+7P3jRsOA0d8So4Cv/OELRoMXo687ZEXqUMCekiXJgpFw9dz12021NFlw==}
has-bigints@1.1.0:
resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
engines: {node: '>= 0.4'}
@@ -9523,6 +9537,10 @@ packages:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
human-id@4.1.3:
resolution: {integrity: sha512-tsYlhAYpjCKa//8rXZ9DqKEawhPoSytweBC2eNvcaDK+57RZLHGqNs3PZTQO6yekLFSuvA6AlnAfrw1uBvtb+Q==}
hasBin: true
human-signals@2.1.0:
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
engines: {node: '>=10.17.0'}
@@ -9826,6 +9844,10 @@ packages:
resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
engines: {node: '>= 0.4'}
is-subdir@1.2.0:
resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==}
engines: {node: '>=4'}
is-symbol@1.1.1:
resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
engines: {node: '>= 0.4'}
@@ -9864,6 +9886,10 @@ packages:
resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==}
engines: {node: '>=12.13'}
is-windows@1.0.2:
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
engines: {node: '>=0.10.0'}
is-wsl@2.2.0:
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
engines: {node: '>=8'}
@@ -10230,6 +10256,9 @@ packages:
engines: {node: '>=6'}
hasBin: true
jsonfile@4.0.0:
resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==}
jsonfile@6.2.0:
resolution: {integrity: sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==}
@@ -10553,6 +10582,9 @@ packages:
lodash.sortby@4.7.0:
resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==}
lodash.startcase@4.4.0:
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
lodash.throttle@4.1.1:
resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==}
@@ -11330,6 +11362,9 @@ packages:
resolution: {integrity: sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==}
engines: {node: '>=18'}
outdent@0.5.0:
resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==}
outvariant@1.4.3:
resolution: {integrity: sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==}
@@ -11337,6 +11372,10 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
p-filter@2.1.0:
resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==}
engines: {node: '>=8'}
p-limit@2.3.0:
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
engines: {node: '>=6'}
@@ -11353,6 +11392,10 @@ packages:
resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
engines: {node: '>=10'}
p-map@2.1.0:
resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==}
engines: {node: '>=6'}
p-try@2.2.0:
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
engines: {node: '>=6'}
@@ -11360,6 +11403,9 @@ packages:
package-json-from-dist@1.0.1:
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
package-manager-detector@0.2.11:
resolution: {integrity: sha512-BEnLolu+yuz22S56CU1SUKq3XC3PkwD5wv4ikR4MfGvnRVcmzXR9DwSlW2fEamyTPyXHomBJRzgapeuBvRNzJQ==}
package-manager-detector@1.6.0:
resolution: {integrity: sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==}
@@ -11490,6 +11536,10 @@ packages:
engines: {node: '>=0.10'}
hasBin: true
pify@4.0.1:
resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==}
engines: {node: '>=6'}
pirates@4.0.7:
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
engines: {node: '>= 6'}
@@ -11604,6 +11654,11 @@ packages:
resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
engines: {node: '>= 0.8.0'}
prettier@2.8.8:
resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==}
engines: {node: '>=10.13.0'}
hasBin: true
prettier@3.7.4:
resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==}
engines: {node: '>=14'}
@@ -11693,6 +11748,9 @@ packages:
resolution: {integrity: sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==}
engines: {node: '>=0.6'}
quansync@0.2.11:
resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==}
query-string@7.1.3:
resolution: {integrity: sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==}
engines: {node: '>=6'}
@@ -11940,6 +11998,10 @@ packages:
resolution: {integrity: sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==}
engines: {node: '>=0.10.0'}
read-yaml-file@1.1.0:
resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==}
engines: {node: '>=6'}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
@@ -12479,6 +12541,9 @@ packages:
space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
spawndamnit@3.0.1:
resolution: {integrity: sha512-MmnduQUuHCoFckZoWnXsTg7JaiLBJrKFj9UI2MbRPGaJeVpsLcVBu6P/IGZovziM/YBsellCmsprgNA+w0CzVg==}
split-on-first@1.1.0:
resolution: {integrity: sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==}
engines: {node: '>=6'}
@@ -12822,6 +12887,10 @@ packages:
resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
engines: {node: '>=8'}
term-size@2.2.1:
resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==}
engines: {node: '>=8'}
terminal-link@2.1.1:
resolution: {integrity: sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==}
engines: {node: '>=8'}
@@ -13248,6 +13317,10 @@ packages:
unist-util-visit@5.1.0:
resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==}
universalify@0.1.2:
resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==}
engines: {node: '>= 4.0.0'}
universalify@0.2.0:
resolution: {integrity: sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==}
engines: {node: '>= 4.0.0'}
@@ -14720,6 +14793,150 @@ snapshots:
'@borewit/text-codec@0.2.2':
optional: true
'@changesets/apply-release-plan@7.0.14':
dependencies:
'@changesets/config': 3.1.2
'@changesets/get-version-range-type': 0.4.0
'@changesets/git': 3.0.4
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
detect-indent: 6.1.0
fs-extra: 7.0.1
lodash.startcase: 4.4.0
outdent: 0.5.0
prettier: 2.8.8
resolve-from: 5.0.0
semver: 7.7.3
'@changesets/assemble-release-plan@6.0.9':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.3
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
semver: 7.7.3
'@changesets/changelog-git@0.2.1':
dependencies:
'@changesets/types': 6.1.0
'@changesets/cli@2.29.8(@types/node@22.19.6)':
dependencies:
'@changesets/apply-release-plan': 7.0.14
'@changesets/assemble-release-plan': 6.0.9
'@changesets/changelog-git': 0.2.1
'@changesets/config': 3.1.2
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.3
'@changesets/get-release-plan': 4.0.14
'@changesets/git': 3.0.4
'@changesets/logger': 0.1.1
'@changesets/pre': 2.0.2
'@changesets/read': 0.6.6
'@changesets/should-skip-package': 0.1.2
'@changesets/types': 6.1.0
'@changesets/write': 0.4.0
'@inquirer/external-editor': 1.0.3(@types/node@22.19.6)
'@manypkg/get-packages': 1.1.3
ansi-colors: 4.1.3
ci-info: 3.9.0
enquirer: 2.4.1
fs-extra: 7.0.1
mri: 1.2.0
p-limit: 2.3.0
package-manager-detector: 0.2.11
picocolors: 1.1.1
resolve-from: 5.0.0
semver: 7.7.3
spawndamnit: 3.0.1
term-size: 2.2.1
transitivePeerDependencies:
- '@types/node'
'@changesets/config@3.1.2':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/get-dependents-graph': 2.1.3
'@changesets/logger': 0.1.1
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
micromatch: 4.0.8
'@changesets/errors@0.2.0':
dependencies:
extendable-error: 0.1.7
'@changesets/get-dependents-graph@2.1.3':
dependencies:
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
picocolors: 1.1.1
semver: 7.7.3
'@changesets/get-release-plan@4.0.14':
dependencies:
'@changesets/assemble-release-plan': 6.0.9
'@changesets/config': 3.1.2
'@changesets/pre': 2.0.2
'@changesets/read': 0.6.6
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
'@changesets/get-version-range-type@0.4.0': {}
'@changesets/git@3.0.4':
dependencies:
'@changesets/errors': 0.2.0
'@manypkg/get-packages': 1.1.3
is-subdir: 1.2.0
micromatch: 4.0.8
spawndamnit: 3.0.1
'@changesets/logger@0.1.1':
dependencies:
picocolors: 1.1.1
'@changesets/parse@0.4.2':
dependencies:
'@changesets/types': 6.1.0
js-yaml: 4.1.1
'@changesets/pre@2.0.2':
dependencies:
'@changesets/errors': 0.2.0
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
fs-extra: 7.0.1
'@changesets/read@0.6.6':
dependencies:
'@changesets/git': 3.0.4
'@changesets/logger': 0.1.1
'@changesets/parse': 0.4.2
'@changesets/types': 6.1.0
fs-extra: 7.0.1
p-filter: 2.1.0
picocolors: 1.1.1
'@changesets/should-skip-package@0.1.2':
dependencies:
'@changesets/types': 6.1.0
'@manypkg/get-packages': 1.1.3
'@changesets/types@4.1.0': {}
'@changesets/types@6.1.0': {}
'@changesets/write@0.4.0':
dependencies:
'@changesets/types': 6.1.0
fs-extra: 7.0.1
human-id: 4.1.3
prettier: 2.8.8
'@csstools/color-helpers@5.1.0': {}
'@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)':
@@ -15750,6 +15967,13 @@ snapshots:
optionalDependencies:
'@types/node': 22.19.6
'@inquirer/external-editor@1.0.3(@types/node@22.19.6)':
dependencies:
chardet: 2.1.1
iconv-lite: 0.7.2
optionalDependencies:
'@types/node': 22.19.6
'@inquirer/figures@1.0.15': {}
'@inquirer/type@3.0.10(@types/node@22.19.6)':
@@ -16053,6 +16277,22 @@ snapshots:
dependencies:
svelte: 5.54.1
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.28.6
'@types/node': 12.20.55
find-up: 4.1.0
fs-extra: 8.1.0
'@manypkg/get-packages@1.1.3':
dependencies:
'@babel/runtime': 7.28.6
'@changesets/types': 4.1.0
'@manypkg/find-root': 1.1.0
fs-extra: 8.1.0
globby: 11.1.0
read-yaml-file: 1.1.0
'@mdx-js/loader@3.1.1(webpack@5.96.1)':
dependencies:
'@mdx-js/mdx': 3.1.1
@@ -19130,7 +19370,7 @@ snapshots:
'@stripe/ui-extension-tools@0.0.1(@babel/core@7.29.0)(babel-jest@27.5.1(@babel/core@7.29.0))':
dependencies:
'@types/jest': 28.1.8
'@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
'@typescript-eslint/eslint-plugin': 5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
eslint: 8.57.1
eslint-plugin-react: 7.37.5(eslint@8.57.1)
@@ -19688,6 +19928,8 @@ snapshots:
'@types/ms@2.1.0': {}
'@types/node@12.20.55': {}
'@types/node@22.19.6':
dependencies:
undici-types: 6.21.0
@@ -19797,7 +20039,7 @@ snapshots:
'@types/node': 22.19.6
optional: true
'@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@9.39.2(jiti@2.6.1))(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
'@typescript-eslint/eslint-plugin@5.62.0(@typescript-eslint/parser@5.62.0(eslint@8.57.1)(typescript@4.9.5))(eslint@8.57.1)(typescript@4.9.5)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
@@ -20457,6 +20699,8 @@ snapshots:
anser@1.4.10: {}
ansi-colors@4.1.3: {}
ansi-escapes@4.3.2:
dependencies:
type-fest: 0.21.3
@@ -20803,6 +21047,10 @@ snapshots:
dependencies:
open: 8.4.2
better-path-resolve@1.0.0:
dependencies:
is-windows: 1.0.2
bidi-js@1.0.3:
dependencies:
require-from-string: 2.0.2
@@ -21003,6 +21251,8 @@ snapshots:
character-reference-invalid@2.0.1: {}
chardet@2.1.1: {}
chokidar@4.0.3:
dependencies:
readdirp: 4.1.2
@@ -21443,6 +21693,8 @@ snapshots:
dependencies:
webgl-constants: 1.1.1
detect-indent@6.1.0: {}
detect-libc@2.1.2: {}
detect-newline@3.1.0: {}
@@ -21613,6 +21865,11 @@ snapshots:
graceful-fs: 4.2.11
tapable: 2.3.0
enquirer@2.4.1:
dependencies:
ansi-colors: 4.1.3
strip-ansi: 6.0.1
entities@4.5.0: {}
entities@6.0.1: {}
@@ -22535,6 +22792,8 @@ snapshots:
extend@3.0.2: {}
extendable-error@0.1.7: {}
extract-zip@2.0.1:
dependencies:
debug: 4.4.3
@@ -22746,6 +23005,18 @@ snapshots:
jsonfile: 6.2.0
universalify: 2.0.1
fs-extra@7.0.1:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-extra@8.1.0:
dependencies:
graceful-fs: 4.2.11
jsonfile: 4.0.0
universalify: 0.1.2
fs-monkey@1.0.3: {}
fs.realpath@1.0.0: {}
@@ -22922,8 +23193,6 @@ snapshots:
section-matter: 1.0.0
strip-bom-string: 1.0.0
gsplat@1.2.9: {}
has-bigints@1.1.0: {}
has-flag@3.0.0: {}
@@ -23164,6 +23433,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
human-id@4.1.3: {}
human-signals@2.1.0: {}
human-signals@8.0.1: {}
@@ -23464,6 +23735,10 @@ snapshots:
call-bound: 1.0.4
has-tostringtag: 1.0.2
is-subdir@1.2.0:
dependencies:
better-path-resolve: 1.0.0
is-symbol@1.1.1:
dependencies:
call-bound: 1.0.4
@@ -23495,6 +23770,8 @@ snapshots:
is-what@4.1.16: {}
is-windows@1.0.2: {}
is-wsl@2.2.0:
dependencies:
is-docker: 2.2.1
@@ -24168,6 +24445,10 @@ snapshots:
json5@2.2.3: {}
jsonfile@4.0.0:
optionalDependencies:
graceful-fs: 4.2.11
jsonfile@6.2.0:
dependencies:
universalify: 2.0.1
@@ -24442,6 +24723,8 @@ snapshots:
lodash.sortby@4.7.0: {}
lodash.startcase@4.4.0: {}
lodash.throttle@4.1.1: {}
lodash@4.17.23: {}
@@ -25400,7 +25683,7 @@ snapshots:
dependencies:
'@next/env': 16.1.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.10
baseline-browser-mapping: 2.9.14
caniuse-lite: 1.0.30001764
postcss: 8.4.31
react: 19.2.4
@@ -25637,6 +25920,8 @@ snapshots:
string-width: 7.2.0
strip-ansi: 7.1.2
outdent@0.5.0: {}
outvariant@1.4.3: {}
own-keys@1.0.1:
@@ -25645,6 +25930,10 @@ snapshots:
object-keys: 1.1.1
safe-push-apply: 1.0.0
p-filter@2.1.0:
dependencies:
p-map: 2.1.0
p-limit@2.3.0:
dependencies:
p-try: 2.2.0
@@ -25661,10 +25950,16 @@ snapshots:
dependencies:
p-limit: 3.1.0
p-map@2.1.0: {}
p-try@2.2.0: {}
package-json-from-dist@1.0.1: {}
package-manager-detector@0.2.11:
dependencies:
quansync: 0.2.11
package-manager-detector@1.6.0: {}
pako@0.2.9: {}
@@ -25774,6 +26069,8 @@ snapshots:
pidtree@0.6.0: {}
pify@4.0.1: {}
pirates@4.0.7: {}
pkce-challenge@5.0.1: {}
@@ -25890,6 +26187,8 @@ snapshots:
prelude-ls@1.2.1: {}
prettier@2.8.8: {}
prettier@3.7.4: {}
pretty-bytes@5.6.0: {}
@@ -25990,6 +26289,8 @@ snapshots:
dependencies:
side-channel: 1.1.0
quansync@0.2.11: {}
query-string@7.1.3:
dependencies:
decode-uri-component: 0.2.2
@@ -26561,6 +26862,13 @@ snapshots:
react@19.2.4: {}
read-yaml-file@1.1.0:
dependencies:
graceful-fs: 4.2.11
js-yaml: 3.14.2
pify: 4.0.1
strip-bom: 3.0.0
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
@@ -27352,6 +27660,11 @@ snapshots:
space-separated-tokens@2.0.2: {}
spawndamnit@3.0.1:
dependencies:
cross-spawn: 7.0.6
signal-exit: 4.1.0
split-on-first@1.1.0: {}
sprintf-js@1.0.3: {}
@@ -27807,6 +28120,8 @@ snapshots:
temp-dir@2.0.0: {}
term-size@2.2.1: {}
terminal-link@2.1.1:
dependencies:
ansi-escapes: 4.3.2
@@ -28304,6 +28619,8 @@ snapshots:
unist-util-is: 6.0.1
unist-util-visit-parents: 6.0.2
universalify@0.1.2: {}
universalify@0.2.0: {}
universalify@2.0.1: {}
-38
View File
@@ -1,38 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.resolve(__dirname, "..");
const CORE_PKG = path.join(ROOT, "packages/core/package.json");
const core = JSON.parse(fs.readFileSync(CORE_PKG, "utf8"));
const version = core.version;
console.log(`Canonical version (from @json-render/core): ${version}`);
const packagesDir = path.join(ROOT, "packages");
const mismatches = [];
for (const dir of fs.readdirSync(packagesDir)) {
const pkgPath = path.join(packagesDir, dir, "package.json");
if (!fs.existsSync(pkgPath)) continue;
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
if (!pkg.name || !pkg.name.startsWith("@json-render/")) continue;
if (pkg.version !== version) {
mismatches.push({ name: pkg.name, version: pkg.version });
}
}
if (mismatches.length > 0) {
console.error("\nVersion mismatch detected:");
for (const m of mismatches) {
console.error(` ${m.name}: ${m.version} (expected ${version})`);
}
console.error("\nRun `pnpm run version:sync` to fix.");
process.exit(1);
} else {
console.log("All @json-render/* packages are in sync.");
}
-35
View File
@@ -1,35 +0,0 @@
#!/usr/bin/env node
const fs = require("fs");
const path = require("path");
const ROOT = path.resolve(__dirname, "..");
const CORE_PKG = path.join(ROOT, "packages/core/package.json");
const core = JSON.parse(fs.readFileSync(CORE_PKG, "utf8"));
const version = core.version;
console.log(`Canonical version (from @json-render/core): ${version}`);
const packagesDir = path.join(ROOT, "packages");
let updated = 0;
for (const dir of fs.readdirSync(packagesDir)) {
const pkgPath = path.join(packagesDir, dir, "package.json");
if (!fs.existsSync(pkgPath)) continue;
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
if (!pkg.name || !pkg.name.startsWith("@json-render/")) continue;
if (pkg.version === version) continue;
console.log(` ${pkg.name}: ${pkg.version} -> ${version}`);
pkg.version = version;
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
updated++;
}
if (updated === 0) {
console.log("All @json-render/* packages are already in sync.");
} else {
console.log(`Updated ${updated} package(s).`);
}
+1 -8
View File
@@ -6,13 +6,7 @@
"build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".next/**", "!.next/cache/**", "dist/**", ".svelte-kit/**"]
},
"svelte-chat#build": {
"dependsOn": ["^build"],
"inputs": ["$TURBO_DEFAULT$", ".env*"],
"outputs": [".svelte-kit/**"],
"cache": false
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
},
"lint": {
"dependsOn": ["^lint"]
@@ -21,7 +15,6 @@
"dependsOn": ["^build"]
},
"dev": {
"dependsOn": ["^build"],
"cache": false,
"persistent": true
}