mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 04:18:15 +08:00
Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95e7235ee9 | ||
|
|
ee596e4901 | ||
|
|
c604e5983c | ||
|
|
5f2ecc1118 | ||
|
|
6e5ea186de | ||
|
|
a9858918f8 | ||
|
|
eebc6e4f4f | ||
|
|
a123f56fe3 | ||
|
|
753c1d1109 |
@@ -1,13 +0,0 @@
|
||||
# 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.
|
||||
@@ -1,39 +0,0 @@
|
||||
{
|
||||
"$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
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,18 @@ 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
|
||||
|
||||
+127
-17
@@ -8,19 +8,62 @@ on:
|
||||
|
||||
concurrency: ${{ github.workflow }}-${{ github.ref }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
check-release:
|
||||
name: Check for new version
|
||||
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
|
||||
@@ -28,20 +71,87 @@ jobs:
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
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
|
||||
cache: pnpm
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- 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"
|
||||
- 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
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_VERCEL_TOKEN_ELEVATED }}
|
||||
|
||||
@@ -29,6 +29,8 @@ build
|
||||
dist
|
||||
*.tsbuildinfo
|
||||
.svelte-kit/
|
||||
tsup.config.bundled_*.mjs
|
||||
next-env.d.ts
|
||||
|
||||
|
||||
# Debug
|
||||
|
||||
@@ -73,25 +73,20 @@ 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)
|
||||
|
||||
## Releases
|
||||
## Releasing
|
||||
|
||||
This monorepo uses [Changesets](https://github.com/changesets/changesets) for versioning and publishing.
|
||||
Releases are manual, single-PR affairs. The maintainer controls the changelog voice and format.
|
||||
|
||||
### 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.
|
||||
All public `@json-render/*` packages share the same version. The canonical version lives in `packages/core/package.json`.
|
||||
|
||||
### Preparing a release
|
||||
|
||||
When asked to prepare a release (e.g. "prepare v0.12.0"):
|
||||
When asked to prepare a release (e.g. "prepare v0.17.0"):
|
||||
|
||||
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
|
||||
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)
|
||||
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)
|
||||
@@ -101,12 +96,15 @@ When asked to prepare a release (e.g. "prepare v0.12.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 scripts
|
||||
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.
|
||||
|
||||
- `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)
|
||||
### 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)
|
||||
|
||||
<!-- opensrc:start -->
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
# 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
|
||||
@@ -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
|
||||
# or for 3D scenes (and gaussian splatting via the GaussianSplat component)
|
||||
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 (19 built-in components) |
|
||||
| `@json-render/react-three-fiber` | React Three Fiber renderer for 3D scenes (20 built-in components, including GaussianSplat) |
|
||||
| `@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,6 +462,7 @@ const catalog = defineCatalog(schema, {
|
||||
Sphere: threeComponentDefinitions.Sphere,
|
||||
AmbientLight: threeComponentDefinitions.AmbientLight,
|
||||
DirectionalLight: threeComponentDefinitions.DirectionalLight,
|
||||
GaussianSplat: threeComponentDefinitions.GaussianSplat,
|
||||
OrbitControls: threeComponentDefinitions.OrbitControls,
|
||||
},
|
||||
actions: {},
|
||||
@@ -473,6 +474,7 @@ const { registry } = defineRegistry(catalog, {
|
||||
Sphere: threeComponents.Sphere,
|
||||
AmbientLight: threeComponents.AmbientLight,
|
||||
DirectionalLight: threeComponents.DirectionalLight,
|
||||
GaussianSplat: threeComponents.GaussianSplat,
|
||||
OrbitControls: threeComponents.OrbitControls,
|
||||
},
|
||||
});
|
||||
@@ -730,6 +732,8 @@ 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. 19 built-in 3D components for meshes, lights, models, environments, text, cameras, and controls.
|
||||
React Three Fiber renderer for json-render. 20 built-in 3D components for meshes, lights, models, gaussian splats, environments, text, cameras, and controls.
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -283,6 +283,11 @@ 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>
|
||||
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { Header } from "@/components/header";
|
||||
import { getStarCount } from "@/lib/github";
|
||||
|
||||
export default function MainLayout({
|
||||
export default async function MainLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const stars = await getStarCount();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<Header stars={stars} />
|
||||
<main className="flex-1">{children}</main>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -17,10 +17,13 @@ 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.",
|
||||
"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.",
|
||||
"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;
|
||||
|
||||
@@ -18,65 +18,62 @@ import { PlaygroundRenderer } from "@/lib/render/renderer";
|
||||
import { playgroundCatalog } from "@/lib/render/catalog";
|
||||
import { buildCatalogDisplayData } from "@/lib/render/catalog-display";
|
||||
|
||||
const SIMULATION_PROMPT = "Create a contact form with name, email, and message";
|
||||
const SIMULATION_PROMPT = "Show a team performance dashboard";
|
||||
|
||||
interface SimulationStage {
|
||||
tree: Spec;
|
||||
stream: string;
|
||||
}
|
||||
|
||||
// Shared state & element definitions for the progressive simulation stages.
|
||||
const FORM_STATE = { form: { name: "", email: "", message: "" } };
|
||||
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 },
|
||||
],
|
||||
};
|
||||
|
||||
const NAME_INPUT = {
|
||||
type: "Input",
|
||||
const METRIC_REVENUE = {
|
||||
type: "Metric",
|
||||
props: {
|
||||
label: "Name",
|
||||
name: "name",
|
||||
statePath: "/form/name",
|
||||
checks: [{ type: "required", message: "Name is required" }],
|
||||
label: "Weekly Revenue",
|
||||
value: "12,400",
|
||||
prefix: "$",
|
||||
change: "+18%",
|
||||
changeType: "positive",
|
||||
},
|
||||
} as const;
|
||||
|
||||
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" },
|
||||
],
|
||||
},
|
||||
const CHART = {
|
||||
type: "LineGraph",
|
||||
props: { data: { $state: "/chartData" } },
|
||||
} as const;
|
||||
|
||||
const MESSAGE_INPUT = {
|
||||
type: "Textarea",
|
||||
props: {
|
||||
label: "Message",
|
||||
name: "message",
|
||||
statePath: "/form/message",
|
||||
checks: [{ type: "required", message: "Message is required" }],
|
||||
},
|
||||
const SEP = { type: "Separator", props: {} } as const;
|
||||
|
||||
const PROGRESS_DEALS = {
|
||||
type: "Progress",
|
||||
props: { value: 72, label: "Deals Closed -- 72%" },
|
||||
} as const;
|
||||
|
||||
const SUBMIT_BUTTON = {
|
||||
type: "Button",
|
||||
props: { label: "Send Message", variant: "primary" },
|
||||
on: { press: { action: "formSubmit" } },
|
||||
const PROGRESS_RETENTION = {
|
||||
type: "Progress",
|
||||
props: { value: 91, label: "Retention -- 91%" },
|
||||
} as const;
|
||||
|
||||
const SIMULATION_STAGES: SimulationStage[] = [
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
state: FORM_STATE,
|
||||
state: DASH_STATE,
|
||||
elements: {
|
||||
card: {
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
props: { title: "Team Performance", maxWidth: "sm", centered: true },
|
||||
children: [],
|
||||
},
|
||||
},
|
||||
@@ -86,72 +83,74 @@ const SIMULATION_STAGES: SimulationStage[] = [
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
state: FORM_STATE,
|
||||
state: DASH_STATE,
|
||||
elements: {
|
||||
card: {
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name"],
|
||||
props: { title: "Team Performance", maxWidth: "sm", centered: true },
|
||||
children: ["m1"],
|
||||
},
|
||||
name: NAME_INPUT,
|
||||
m1: METRIC_REVENUE,
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/name","value":{"type":"Input","props":{"label":"Name","name":"name","statePath":"/form/name","checks":[{"type":"required","message":"Name is required"}]}}}',
|
||||
'{"op":"add","path":"/elements/m1","value":{"type":"Metric","props":{"label":"Weekly Revenue","value":"12,400","prefix":"$","change":"+18%","changeType":"positive"}}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
state: FORM_STATE,
|
||||
state: DASH_STATE,
|
||||
elements: {
|
||||
card: {
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email"],
|
||||
props: { title: "Team Performance", maxWidth: "sm", centered: true },
|
||||
children: ["m1", "chart"],
|
||||
},
|
||||
name: NAME_INPUT,
|
||||
email: EMAIL_INPUT,
|
||||
m1: METRIC_REVENUE,
|
||||
chart: CHART,
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"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"}]}}}',
|
||||
'{"op":"add","path":"/elements/chart","value":{"type":"LineGraph","props":{"data":{"$state":"/chartData"}}}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
state: FORM_STATE,
|
||||
state: DASH_STATE,
|
||||
elements: {
|
||||
card: {
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email", "message"],
|
||||
props: { title: "Team Performance", maxWidth: "sm", centered: true },
|
||||
children: ["m1", "chart", "sep", "p1"],
|
||||
},
|
||||
name: NAME_INPUT,
|
||||
email: EMAIL_INPUT,
|
||||
message: MESSAGE_INPUT,
|
||||
m1: METRIC_REVENUE,
|
||||
chart: CHART,
|
||||
sep: SEP,
|
||||
p1: PROGRESS_DEALS,
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/message","value":{"type":"Textarea","props":{"label":"Message","name":"message","statePath":"/form/message","checks":[{"type":"required","message":"Message is required"}]}}}',
|
||||
'{"op":"add","path":"/elements/p1","value":{"type":"Progress","props":{"value":72,"label":"Deals Closed -- 72%"}}}',
|
||||
},
|
||||
{
|
||||
tree: {
|
||||
root: "card",
|
||||
state: FORM_STATE,
|
||||
state: DASH_STATE,
|
||||
elements: {
|
||||
card: {
|
||||
type: "Card",
|
||||
props: { title: "Contact Us", maxWidth: "md" },
|
||||
children: ["name", "email", "message", "submit"],
|
||||
props: { title: "Team Performance", maxWidth: "sm", centered: true },
|
||||
children: ["m1", "chart", "sep", "p1", "p2"],
|
||||
},
|
||||
name: NAME_INPUT,
|
||||
email: EMAIL_INPUT,
|
||||
message: MESSAGE_INPUT,
|
||||
submit: SUBMIT_BUTTON,
|
||||
m1: METRIC_REVENUE,
|
||||
chart: CHART,
|
||||
sep: SEP,
|
||||
p1: PROGRESS_DEALS,
|
||||
p2: PROGRESS_RETENTION,
|
||||
},
|
||||
},
|
||||
stream:
|
||||
'{"op":"add","path":"/elements/submit","value":{"type":"Button","props":{"label":"Send Message","variant":"primary"},"on":{"press":{"action":"formSubmit"}}}}',
|
||||
'{"op":"add","path":"/elements/p2","value":{"type":"Progress","props":{"value":91,"label":"Retention -- 91%"}}}',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -211,10 +210,10 @@ function specToNested(spec: Spec): Record<string, unknown> {
|
||||
}
|
||||
|
||||
const EXAMPLE_PROMPTS = [
|
||||
"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",
|
||||
"Recipe card with rating and ingredients",
|
||||
"Order receipt with item list and total",
|
||||
"Team member profile card",
|
||||
"Notification inbox with alerts",
|
||||
];
|
||||
|
||||
export function Demo({
|
||||
@@ -1117,7 +1116,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-[28rem]"}`}
|
||||
className={`border border-border rounded bg-background font-mono text-xs text-left grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[36rem]"}`}
|
||||
>
|
||||
{activeTab !== "catalog" && (
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
@@ -1357,7 +1356,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-[28rem]"}`}
|
||||
className={`border border-border rounded bg-background grid relative group ${fullscreen ? "flex-1 min-h-0" : "h-[36rem]"}`}
|
||||
>
|
||||
{renderView === "static" && (
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
|
||||
@@ -19,7 +19,13 @@ const navLinks = [
|
||||
{ href: "/docs", label: "Docs" },
|
||||
];
|
||||
|
||||
function GitHubLink({ className }: { className?: string }) {
|
||||
function GitHubLink({
|
||||
className,
|
||||
stars,
|
||||
}: {
|
||||
className?: string;
|
||||
stars?: string;
|
||||
}) {
|
||||
return (
|
||||
<a
|
||||
href="https://github.com/vercel-labs/json-render"
|
||||
@@ -38,12 +44,12 @@ function GitHubLink({ className }: { className?: string }) {
|
||||
>
|
||||
<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>
|
||||
<span>12k</span>
|
||||
{stars && <span>{stars}</span>}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function Header() {
|
||||
export function Header({ stars }: { stars?: string }) {
|
||||
const pathname = usePathname();
|
||||
const [mobileOpen, setMobileOpen] = useState(false);
|
||||
|
||||
@@ -122,14 +128,14 @@ export function Header() {
|
||||
</Link>
|
||||
))}
|
||||
<Search />
|
||||
<GitHubLink />
|
||||
<GitHubLink stars={stars} />
|
||||
<ThemeToggle />
|
||||
</nav>
|
||||
|
||||
{/* Mobile nav */}
|
||||
<div className="flex sm:hidden items-center gap-3">
|
||||
<Search />
|
||||
<GitHubLink />
|
||||
<GitHubLink stars={stars} />
|
||||
<Sheet open={mobileOpen} onOpenChange={setMobileOpen}>
|
||||
<SheetTrigger
|
||||
className="flex items-center justify-center"
|
||||
|
||||
@@ -108,6 +108,22 @@ 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",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
@@ -165,7 +165,8 @@ export const playgroundCatalog = defineCatalog(schema, {
|
||||
.enum(["body", "caption", "muted", "lead", "code"])
|
||||
.nullable(),
|
||||
}),
|
||||
description: "Paragraph text",
|
||||
description:
|
||||
'Paragraph text. In repeat scopes, use { "$template": "${field1} ${field2}" } to interpolate item fields.',
|
||||
example: { text: "Hello, world!" },
|
||||
},
|
||||
|
||||
@@ -178,6 +179,18 @@ 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(),
|
||||
@@ -259,8 +272,32 @@ export const playgroundCatalog = defineCatalog(schema, {
|
||||
value: z.number(),
|
||||
max: z.number().nullable(),
|
||||
label: z.string().nullable(),
|
||||
interactive: z.boolean().nullable(),
|
||||
}),
|
||||
description: "Star rating display",
|
||||
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",
|
||||
},
|
||||
},
|
||||
|
||||
// ── Charts ──────────────────────────────────────────────────────────
|
||||
@@ -411,11 +448,14 @@ export const playgroundCatalog = defineCatalog(schema, {
|
||||
Button: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
variant: z.enum(["primary", "secondary", "danger"]).nullable(),
|
||||
variant: z
|
||||
.enum(["primary", "secondary", "outline", "danger"])
|
||||
.nullable(),
|
||||
disabled: z.boolean().nullable(),
|
||||
}),
|
||||
events: ["press"],
|
||||
description: "Clickable button. Bind on.press for handler.",
|
||||
description:
|
||||
"Clickable button. primary = solid fill, outline = bordered/transparent, secondary = muted fill. Bind on.press for handler.",
|
||||
example: { label: "Submit", variant: "primary" },
|
||||
},
|
||||
|
||||
|
||||
+355
-124
@@ -105,6 +105,7 @@ import {
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { icons as lucideIcons } from "lucide-react";
|
||||
|
||||
// =============================================================================
|
||||
// Registry — components + actions, types inferred from catalog
|
||||
@@ -127,23 +128,25 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`border border-border rounded-lg p-4 bg-card text-card-foreground overflow-hidden ${maxWidthClass} ${centeredClass}`}
|
||||
className={`border border-border rounded-xl p-5 bg-card text-card-foreground shadow-sm overflow-hidden h-full flex flex-col ${maxWidthClass} ${centeredClass}`}
|
||||
>
|
||||
{(props.title || props.description) && (
|
||||
<div className="mb-4">
|
||||
{props.title && (
|
||||
<h3 className="font-semibold text-lg text-left">
|
||||
<h3 className="font-semibold text-lg tracking-tight">
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
{props.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1 text-left">
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{props.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3">{children}</div>
|
||||
<div className="flex-1 flex flex-col gap-4 [&>:last-child]:mt-auto">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -160,14 +163,31 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
: props.gap === "none"
|
||||
? "gap-0"
|
||||
: "gap-3";
|
||||
const alignClass =
|
||||
props.align === "center"
|
||||
? "items-center"
|
||||
: props.align === "end"
|
||||
? "items-end"
|
||||
: props.align === "stretch"
|
||||
? "items-stretch"
|
||||
: "items-start";
|
||||
|
||||
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 justifyClass =
|
||||
props.justify === "center"
|
||||
? "justify-center"
|
||||
@@ -181,7 +201,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`flex ${isHorizontal ? "flex-row flex-wrap" : "flex-col"} ${gapClass} ${alignClass} ${justifyClass}`}
|
||||
className={`flex ${isHorizontal ? "flex-row flex-wrap" : "flex-col w-full"} ${gapClass} ${alignClass} ${justifyClass}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
@@ -189,7 +209,12 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
},
|
||||
|
||||
Grid: ({ props, children }) => {
|
||||
const n = props.columns ?? 1;
|
||||
const childCount = Array.isArray(children)
|
||||
? children.length
|
||||
: children
|
||||
? 1
|
||||
: 0;
|
||||
const n = Math.min(props.columns ?? 1, childCount || 1);
|
||||
const cols =
|
||||
n >= 6
|
||||
? "grid-cols-6"
|
||||
@@ -205,13 +230,15 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const gridGap =
|
||||
props.gap === "lg" ? "gap-4" : props.gap === "sm" ? "gap-2" : "gap-3";
|
||||
|
||||
return <div className={`grid ${cols} ${gridGap}`}>{children}</div>;
|
||||
return <div className={`grid w-full ${cols} ${gridGap}`}>{children}</div>;
|
||||
},
|
||||
|
||||
Separator: ({ props }) => (
|
||||
<Separator
|
||||
orientation={props.orientation ?? "horizontal"}
|
||||
className={props.orientation === "vertical" ? "h-full mx-2" : "my-3"}
|
||||
className={
|
||||
props.orientation === "vertical" ? "h-full mx-3" : "my-4 opacity-50"
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -383,7 +410,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="rounded-md border border-border overflow-hidden">
|
||||
<div className="w-full rounded-md border border-border overflow-hidden">
|
||||
<TablePrimitive>
|
||||
{props.caption && <TableCaption>{props.caption}</TableCaption>}
|
||||
<TableHeader>
|
||||
@@ -411,20 +438,17 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const level = props.level ?? "h2";
|
||||
const headingClass =
|
||||
level === "h1"
|
||||
? "text-2xl font-bold"
|
||||
? "text-2xl font-bold tracking-tight"
|
||||
: level === "h3"
|
||||
? "text-base font-semibold"
|
||||
? "text-base font-semibold tracking-tight"
|
||||
: level === "h4"
|
||||
? "text-sm font-semibold"
|
||||
: "text-lg font-semibold";
|
||||
? "text-sm font-medium uppercase tracking-wider text-muted-foreground"
|
||||
: "text-xl font-semibold tracking-tight";
|
||||
|
||||
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>;
|
||||
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>;
|
||||
},
|
||||
|
||||
Text: ({ props }) => {
|
||||
@@ -440,20 +464,59 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
: "text-sm";
|
||||
|
||||
if (props.variant === "code") {
|
||||
return <code className={`${textClass} text-left`}>{props.text}</code>;
|
||||
return <code className={textClass}>{props.text}</code>;
|
||||
}
|
||||
return <p className={`${textClass} text-left`}>{props.text}</p>;
|
||||
return <p className={textClass}>{props.text}</p>;
|
||||
},
|
||||
|
||||
Image: ({ props }) => (
|
||||
<div
|
||||
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 }}
|
||||
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,
|
||||
}}
|
||||
>
|
||||
{props.alt || "img"}
|
||||
<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>}
|
||||
</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
|
||||
@@ -462,16 +525,16 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
.join("")
|
||||
.slice(0, 2)
|
||||
.toUpperCase();
|
||||
const avatarSize =
|
||||
const sizeStyles =
|
||||
props.size === "lg"
|
||||
? "w-12 h-12 text-base"
|
||||
? { outer: "w-[72px] h-[72px]", text: "text-xl", ring: "ring-[3px]" }
|
||||
: props.size === "sm"
|
||||
? "w-8 h-8 text-xs"
|
||||
: "w-10 h-10 text-sm";
|
||||
? { outer: "w-8 h-8", text: "text-xs", ring: "ring-2" }
|
||||
: { outer: "w-10 h-10", text: "text-sm", ring: "ring-2" };
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${avatarSize} rounded-full bg-muted flex items-center justify-center font-medium`}
|
||||
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`}
|
||||
>
|
||||
{initials}
|
||||
</div>
|
||||
@@ -491,9 +554,20 @@ 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}>
|
||||
<Badge variant={variant} className={`${customClass} gap-1.5`}>
|
||||
{dotColor && (
|
||||
<span className={`w-1.5 h-1.5 rounded-full ${dotColor} shrink-0`} />
|
||||
)}
|
||||
{props.text}
|
||||
</Badge>
|
||||
);
|
||||
@@ -510,8 +584,47 @@ 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>
|
||||
@@ -523,7 +636,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
Progress: ({ props }) => {
|
||||
const value = Math.min(100, Math.max(0, props.value || 0));
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{props.label && (
|
||||
<Label className="text-sm text-muted-foreground">
|
||||
{props.label}
|
||||
@@ -607,9 +720,19 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
</PopoverPrimitive>
|
||||
),
|
||||
|
||||
Rating: ({ props }) => {
|
||||
const ratingValue = props.value || 0;
|
||||
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;
|
||||
const maxRating = props.max ?? 5;
|
||||
const interactive = props.interactive !== false;
|
||||
const [hoverIndex, setHoverIndex] = useState(-1);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{props.label && (
|
||||
@@ -617,15 +740,78 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
{props.label}
|
||||
</Label>
|
||||
)}
|
||||
<div className="flex gap-1">
|
||||
{Array.from({ length: maxRating }).map((_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`text-lg ${i < ratingValue ? "text-yellow-400" : "text-muted"}`}
|
||||
>
|
||||
*
|
||||
<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}
|
||||
</span>
|
||||
))}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -636,26 +822,39 @@ 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-2">
|
||||
<div className="space-y-3">
|
||||
{props.title && (
|
||||
<div className="text-sm font-medium text-left">{props.title}</div>
|
||||
<div className="text-sm font-medium">{props.title}</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<div className="flex items-end gap-2" style={{ height: 160 }}>
|
||||
{data.map((d, i) => (
|
||||
<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
|
||||
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
|
||||
className="w-full bg-primary rounded-t transition-all"
|
||||
className={`w-full ${barColors[i % barColors.length]} rounded-t-md transition-all group-hover:opacity-80`}
|
||||
style={{
|
||||
height: `${(d.value / maxValue) * 100}%`,
|
||||
minHeight: 2,
|
||||
minHeight: 4,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground truncate w-full text-center">
|
||||
<div className="text-[11px] text-muted-foreground truncate w-full text-center">
|
||||
{d.label}
|
||||
</div>
|
||||
</div>
|
||||
@@ -672,8 +871,8 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const range = maxValue - minValue || 1;
|
||||
|
||||
const width = 300;
|
||||
const height = 100;
|
||||
const padding = { top: 10, right: 10, bottom: 10, left: 10 };
|
||||
const height = 140;
|
||||
const padding = { top: 12, right: 12, bottom: 12, left: 12 };
|
||||
const chartWidth = width - padding.left - padding.right;
|
||||
const chartHeight = height - padding.top - padding.bottom;
|
||||
|
||||
@@ -690,77 +889,107 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
return { x, y, ...d };
|
||||
});
|
||||
|
||||
const pathD =
|
||||
points.length > 0
|
||||
? `M ${points.map((p) => `${p.x} ${p.y}`).join(" L ")}`
|
||||
: "";
|
||||
// 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)}`;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
{props.title && (
|
||||
<div className="text-sm font-medium text-left">{props.title}</div>
|
||||
<div className="text-sm font-medium">{props.title}</div>
|
||||
)}
|
||||
<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 && (
|
||||
<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 && (
|
||||
<path
|
||||
d={pathD}
|
||||
d={areaPath}
|
||||
fill={`url(#${gradientId})`}
|
||||
className="text-primary"
|
||||
/>
|
||||
)}
|
||||
{smoothPath && (
|
||||
<path
|
||||
d={smoothPath}
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
vectorEffect="non-scaling-stroke"
|
||||
className="text-primary"
|
||||
/>
|
||||
)}
|
||||
{points.map((p, i) => (
|
||||
<circle
|
||||
key={i}
|
||||
cx={p.x}
|
||||
cy={p.y}
|
||||
r="4"
|
||||
className="fill-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>
|
||||
{data.length > 0 && (
|
||||
<div className="flex justify-between">
|
||||
{data.map((d, i) => (
|
||||
<div
|
||||
{points.length > 0 && (
|
||||
<div className="relative h-4">
|
||||
{points.map((p, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="text-xs text-muted-foreground text-center"
|
||||
style={{ width: `${100 / data.length}%` }}
|
||||
className="absolute text-[11px] text-muted-foreground -translate-x-1/2"
|
||||
style={{ left: `${(p.x / width) * 100}%` }}
|
||||
>
|
||||
{d.label}
|
||||
</div>
|
||||
{data[i]?.label}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -787,7 +1016,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{props.label && <Label htmlFor={props.name}>{props.label}</Label>}
|
||||
<Input
|
||||
id={props.name}
|
||||
@@ -829,7 +1058,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{props.label && <Label htmlFor={props.name}>{props.label}</Label>}
|
||||
<Textarea
|
||||
id={props.name}
|
||||
@@ -872,7 +1101,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
<Label>{props.label}</Label>
|
||||
<Select
|
||||
value={value}
|
||||
@@ -945,7 +1174,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const setValue = isBound ? setBoundValue : setLocalValue;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{props.label && <Label>{props.label}</Label>}
|
||||
<RadioGroup
|
||||
value={value}
|
||||
@@ -987,7 +1216,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const setChecked = isBound ? setBoundChecked : setLocalChecked;
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-between space-x-2">
|
||||
<div className="w-full flex items-center justify-between space-x-2">
|
||||
<Label htmlFor={props.name} className="cursor-pointer">
|
||||
{props.label}
|
||||
</Label>
|
||||
@@ -1014,7 +1243,7 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const setValue = isBound ? setBoundValue : setLocalValue;
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="w-full space-y-2">
|
||||
{props.label && (
|
||||
<div className="flex justify-between">
|
||||
<Label className="text-sm">{props.label}</Label>
|
||||
@@ -1041,9 +1270,11 @@ export const { registry, executeAction } = defineRegistry(playgroundCatalog, {
|
||||
const variant =
|
||||
props.variant === "danger"
|
||||
? "destructive"
|
||||
: props.variant === "secondary"
|
||||
? "secondary"
|
||||
: "default";
|
||||
: props.variant === "outline"
|
||||
? "outline"
|
||||
: props.variant === "secondary"
|
||||
? "secondary"
|
||||
: "default";
|
||||
|
||||
return (
|
||||
<Button
|
||||
|
||||
@@ -47,7 +47,7 @@ export function Header() {
|
||||
</div>
|
||||
<nav className="flex items-center gap-4">
|
||||
<a
|
||||
href="https://json-render.vercel.app"
|
||||
href="https://json-render.dev"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
@@ -0,0 +1,421 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
Vendored
+79
@@ -0,0 +1,79 @@
|
||||
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>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
"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 />;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
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];
|
||||
}>;
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
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,
|
||||
];
|
||||
@@ -0,0 +1,41 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,47 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
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],
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,11 @@
|
||||
import { nextJsConfig } from "@internal/eslint-config/next-js";
|
||||
|
||||
/** @type {import("eslint").Linter.Config[]} */
|
||||
export default [
|
||||
...nextJsConfig,
|
||||
{
|
||||
rules: {
|
||||
"react/prop-types": "off",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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"]
|
||||
}
|
||||
@@ -2,6 +2,11 @@ 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
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
@@ -0,0 +1,21 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
"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 />;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import type { Spec } from "@json-render/core";
|
||||
|
||||
export type Scene = { name: string; description: string; spec: Spec };
|
||||
@@ -0,0 +1,166 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,15 @@
|
||||
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,
|
||||
];
|
||||
@@ -0,0 +1,146 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,126 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,157 @@
|
||||
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: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
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",
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: [
|
||||
"@json-render/core",
|
||||
"@json-render/react",
|
||||
"@json-render/react-three-fiber",
|
||||
"three",
|
||||
],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"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
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
Vendored
-6
@@ -1,6 +0,0 @@
|
||||
/// <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
@@ -1,6 +0,0 @@
|
||||
/// <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.
|
||||
+4
-5
@@ -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 20",
|
||||
"dev": "turbo run dev --concurrency 50",
|
||||
"lint": "turbo run lint",
|
||||
"format": "prettier --write \"**/*.{ts,tsx}\"",
|
||||
"type-check": "turbo run check-types",
|
||||
@@ -23,13 +23,12 @@
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:e2e": "pnpm --filter e2e-tests test",
|
||||
"prepare": "husky",
|
||||
"changeset": "changeset",
|
||||
"ci:version": "changeset version && pnpm install --no-frozen-lockfile",
|
||||
"ci:publish": "pnpm run build && changeset publish",
|
||||
"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/*'",
|
||||
"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,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/codegen",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Utilities for generating code from json-render UI trees",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/core",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||
"keywords": [
|
||||
|
||||
@@ -5,7 +5,6 @@ import {
|
||||
resolveBindings,
|
||||
resolveActionParam,
|
||||
_resetWarnedComputedFns,
|
||||
_resetWarnedTemplatePaths,
|
||||
} from "./props";
|
||||
import type { PropResolutionContext } from "./props";
|
||||
|
||||
@@ -635,30 +634,32 @@ describe("$template expressions", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("warns when path does not start with /", () => {
|
||||
_resetWarnedTemplatePaths();
|
||||
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||
it("resolves bare names against state model when not in repeat", () => {
|
||||
const ctx: PropResolutionContext = { stateModel: { name: "Bob" } };
|
||||
const result = resolvePropValue({ $template: "Hi ${name}!" }, ctx);
|
||||
expect(result).toBe("Hi Bob!");
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('$template path "name"'),
|
||||
);
|
||||
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"'),
|
||||
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(calls).toHaveLength(1);
|
||||
warnSpy.mockRestore();
|
||||
_resetWarnedTemplatePaths();
|
||||
expect(result).toBe("Alice - ok");
|
||||
});
|
||||
});
|
||||
|
||||
+18
-24
@@ -24,8 +24,10 @@ 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}` references in the
|
||||
* string with values from the state model
|
||||
* - `{ $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>`.
|
||||
* - Any other value is a literal (passthrough)
|
||||
*/
|
||||
export type PropExpression<T = unknown> =
|
||||
@@ -156,15 +158,6 @@ 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
|
||||
// =============================================================================
|
||||
@@ -269,24 +262,25 @@ export function resolvePropValue(
|
||||
return fn(resolvedArgs);
|
||||
}
|
||||
|
||||
// $template: interpolate ${/path} references with state values
|
||||
// $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>".
|
||||
if (isTemplateExpression(value)) {
|
||||
return value.$template.replace(
|
||||
/\$\{([^}]+)\}/g,
|
||||
(_match, rawPath: string) => {
|
||||
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;
|
||||
if (rawPath.startsWith("/")) {
|
||||
const resolved = getByPath(ctx.stateModel, rawPath);
|
||||
return resolved != null ? String(resolved) : "";
|
||||
}
|
||||
const resolved = getByPath(ctx.stateModel, 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);
|
||||
return resolved != null ? String(resolved) : "";
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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 `${/path}` references in the string with values from the state model.',
|
||||
'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>`.',
|
||||
);
|
||||
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.',
|
||||
' 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.',
|
||||
);
|
||||
lines.push("");
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/image",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/ink",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/jotai",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Jotai adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/mcp",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.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": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/next",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.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": "tsup",
|
||||
"build": "rm -rf dist && tsup",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit"
|
||||
|
||||
@@ -20,7 +20,6 @@ export default defineConfig([
|
||||
dts: true,
|
||||
sourcemap: true,
|
||||
splitting: true,
|
||||
clean: true,
|
||||
banner: { js: '"use client";' },
|
||||
external: sharedExternal,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-email",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-native",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-pdf",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
||||
"keywords": [
|
||||
|
||||
@@ -66,7 +66,7 @@ const { registry } = defineRegistry(catalog, {
|
||||
|
||||
**Environment:** Environment, Fog, GridHelper
|
||||
|
||||
**Text:** Text3D
|
||||
**Text:** Text3D, ExtrudedText
|
||||
|
||||
**Camera/Controls:** PerspectiveCamera, OrbitControls
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-three-fiber",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
|
||||
"keywords": [
|
||||
|
||||
@@ -319,6 +319,32 @@ 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
|
||||
// ===========================================================================
|
||||
@@ -816,6 +842,27 @@ 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,7 +6,10 @@ 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,
|
||||
@@ -350,6 +353,41 @@ 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">>) => (
|
||||
@@ -906,4 +944,24 @@ 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,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/redux",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Redux adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/remotion",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { defineSchema, type PromptContext } from "@json-render/core";
|
||||
import type { z } from "zod";
|
||||
|
||||
/**
|
||||
* Prompt template for Remotion timeline generation
|
||||
@@ -6,7 +7,7 @@ import { defineSchema, type PromptContext } from "@json-render/core";
|
||||
* Uses JSONL patch format (same as React) but builds up a timeline spec structure.
|
||||
*/
|
||||
function remotionPromptTemplate(context: PromptContext): string {
|
||||
const { catalog, options } = context;
|
||||
const { catalog, options, formatZodType } = context;
|
||||
const { system = "You are a video timeline generator.", customRules = [] } =
|
||||
options;
|
||||
|
||||
@@ -37,7 +38,7 @@ function remotionPromptTemplate(context: PromptContext): string {
|
||||
const catalogData = catalog as {
|
||||
components?: Record<
|
||||
string,
|
||||
{ description?: string; defaultDuration?: number }
|
||||
{ description?: string; defaultDuration?: number; props?: z.ZodType }
|
||||
>;
|
||||
transitions?: Record<string, { description?: string }>;
|
||||
effects?: Record<string, { description?: string }>;
|
||||
@@ -52,8 +53,9 @@ function remotionPromptTemplate(context: PromptContext): string {
|
||||
const duration = def.defaultDuration
|
||||
? ` [default: ${def.defaultDuration} frames]`
|
||||
: "";
|
||||
const propsStr = def.props ? ` ${formatZodType(def.props)}` : "";
|
||||
lines.push(
|
||||
`- ${name}: ${def.description || "No description"}${duration}`,
|
||||
`- ${name}:${propsStr} ${def.description || "No description"}${duration}`,
|
||||
);
|
||||
}
|
||||
lines.push("");
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/shadcn-svelte",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "shadcn-svelte component library for @json-render/svelte. JSON becomes beautiful Tailwind-styled Svelte components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/shadcn",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "shadcn/ui component library for @json-render/core. JSON becomes beautiful Tailwind-styled React components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/solid",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "SolidJS renderer for @json-render/core. JSON becomes Solid components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/svelte",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Svelte 5 renderer for @json-render/core. JSON becomes Svelte components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/vue",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Vue renderer for @json-render/core. JSON becomes Vue components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/xstate",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "XState Store adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/yaml",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "YAML wire format for @json-render/core. Progressive rendering and surgical edits via streaming YAML.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/zustand",
|
||||
"version": "0.16.0",
|
||||
"version": "0.17.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Zustand adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
Generated
+97
-414
@@ -8,9 +8,6 @@ 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
|
||||
@@ -568,6 +565,40 @@ 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':
|
||||
@@ -1177,6 +1208,61 @@ 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':
|
||||
@@ -3021,61 +3107,6 @@ 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'}
|
||||
@@ -4158,15 +4189,6 @@ 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'}
|
||||
@@ -4337,12 +4359,6 @@ 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:
|
||||
@@ -6721,9 +6737,6 @@ 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==}
|
||||
|
||||
@@ -7331,10 +7344,6 @@ 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'}
|
||||
@@ -7618,10 +7627,6 @@ 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==}
|
||||
|
||||
@@ -7800,9 +7805,6 @@ 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'}
|
||||
@@ -8296,10 +8298,6 @@ 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'}
|
||||
@@ -8569,10 +8567,6 @@ 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'}
|
||||
@@ -9035,9 +9029,6 @@ 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'}
|
||||
@@ -9212,14 +9203,6 @@ 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==}
|
||||
|
||||
@@ -9390,6 +9373,9 @@ 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'}
|
||||
@@ -9537,10 +9523,6 @@ 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'}
|
||||
@@ -9844,10 +9826,6 @@ 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'}
|
||||
@@ -9886,10 +9864,6 @@ 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'}
|
||||
@@ -10256,9 +10230,6 @@ 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==}
|
||||
|
||||
@@ -10582,9 +10553,6 @@ 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==}
|
||||
|
||||
@@ -11362,9 +11330,6 @@ 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==}
|
||||
|
||||
@@ -11372,10 +11337,6 @@ 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'}
|
||||
@@ -11392,10 +11353,6 @@ 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'}
|
||||
@@ -11403,9 +11360,6 @@ 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==}
|
||||
|
||||
@@ -11536,10 +11490,6 @@ 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'}
|
||||
@@ -11654,11 +11604,6 @@ 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'}
|
||||
@@ -11748,9 +11693,6 @@ 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'}
|
||||
@@ -11998,10 +11940,6 @@ 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'}
|
||||
@@ -12541,9 +12479,6 @@ 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'}
|
||||
@@ -12887,10 +12822,6 @@ 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'}
|
||||
@@ -13317,10 +13248,6 @@ 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'}
|
||||
@@ -14793,150 +14720,6 @@ 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)':
|
||||
@@ -15967,13 +15750,6 @@ 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)':
|
||||
@@ -16277,22 +16053,6 @@ 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
|
||||
@@ -19370,7 +19130,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@8.57.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@9.39.2(jiti@2.6.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)
|
||||
@@ -19928,8 +19688,6 @@ snapshots:
|
||||
|
||||
'@types/ms@2.1.0': {}
|
||||
|
||||
'@types/node@12.20.55': {}
|
||||
|
||||
'@types/node@22.19.6':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
@@ -20039,7 +19797,7 @@ snapshots:
|
||||
'@types/node': 22.19.6
|
||||
optional: true
|
||||
|
||||
'@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/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)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
'@typescript-eslint/parser': 5.62.0(eslint@8.57.1)(typescript@4.9.5)
|
||||
@@ -20699,8 +20457,6 @@ snapshots:
|
||||
|
||||
anser@1.4.10: {}
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-escapes@4.3.2:
|
||||
dependencies:
|
||||
type-fest: 0.21.3
|
||||
@@ -21047,10 +20803,6 @@ 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
|
||||
@@ -21251,8 +21003,6 @@ snapshots:
|
||||
|
||||
character-reference-invalid@2.0.1: {}
|
||||
|
||||
chardet@2.1.1: {}
|
||||
|
||||
chokidar@4.0.3:
|
||||
dependencies:
|
||||
readdirp: 4.1.2
|
||||
@@ -21693,8 +21443,6 @@ snapshots:
|
||||
dependencies:
|
||||
webgl-constants: 1.1.1
|
||||
|
||||
detect-indent@6.1.0: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-newline@3.1.0: {}
|
||||
@@ -21865,11 +21613,6 @@ 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: {}
|
||||
@@ -22792,8 +22535,6 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
extendable-error@0.1.7: {}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
@@ -23005,18 +22746,6 @@ 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: {}
|
||||
@@ -23193,6 +22922,8 @@ 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: {}
|
||||
@@ -23433,8 +23164,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
human-id@4.1.3: {}
|
||||
|
||||
human-signals@2.1.0: {}
|
||||
|
||||
human-signals@8.0.1: {}
|
||||
@@ -23735,10 +23464,6 @@ 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
|
||||
@@ -23770,8 +23495,6 @@ snapshots:
|
||||
|
||||
is-what@4.1.16: {}
|
||||
|
||||
is-windows@1.0.2: {}
|
||||
|
||||
is-wsl@2.2.0:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
@@ -24445,10 +24168,6 @@ snapshots:
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
||||
jsonfile@4.0.0:
|
||||
optionalDependencies:
|
||||
graceful-fs: 4.2.11
|
||||
|
||||
jsonfile@6.2.0:
|
||||
dependencies:
|
||||
universalify: 2.0.1
|
||||
@@ -24723,8 +24442,6 @@ snapshots:
|
||||
|
||||
lodash.sortby@4.7.0: {}
|
||||
|
||||
lodash.startcase@4.4.0: {}
|
||||
|
||||
lodash.throttle@4.1.1: {}
|
||||
|
||||
lodash@4.17.23: {}
|
||||
@@ -25683,7 +25400,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@next/env': 16.1.6
|
||||
'@swc/helpers': 0.5.15
|
||||
baseline-browser-mapping: 2.9.14
|
||||
baseline-browser-mapping: 2.10.10
|
||||
caniuse-lite: 1.0.30001764
|
||||
postcss: 8.4.31
|
||||
react: 19.2.4
|
||||
@@ -25920,8 +25637,6 @@ snapshots:
|
||||
string-width: 7.2.0
|
||||
strip-ansi: 7.1.2
|
||||
|
||||
outdent@0.5.0: {}
|
||||
|
||||
outvariant@1.4.3: {}
|
||||
|
||||
own-keys@1.0.1:
|
||||
@@ -25930,10 +25645,6 @@ 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
|
||||
@@ -25950,16 +25661,10 @@ 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: {}
|
||||
@@ -26069,8 +25774,6 @@ snapshots:
|
||||
|
||||
pidtree@0.6.0: {}
|
||||
|
||||
pify@4.0.1: {}
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pkce-challenge@5.0.1: {}
|
||||
@@ -26187,8 +25890,6 @@ snapshots:
|
||||
|
||||
prelude-ls@1.2.1: {}
|
||||
|
||||
prettier@2.8.8: {}
|
||||
|
||||
prettier@3.7.4: {}
|
||||
|
||||
pretty-bytes@5.6.0: {}
|
||||
@@ -26289,8 +25990,6 @@ snapshots:
|
||||
dependencies:
|
||||
side-channel: 1.1.0
|
||||
|
||||
quansync@0.2.11: {}
|
||||
|
||||
query-string@7.1.3:
|
||||
dependencies:
|
||||
decode-uri-component: 0.2.2
|
||||
@@ -26862,13 +26561,6 @@ 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
|
||||
@@ -27660,11 +27352,6 @@ 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: {}
|
||||
@@ -28120,8 +27807,6 @@ snapshots:
|
||||
|
||||
temp-dir@2.0.0: {}
|
||||
|
||||
term-size@2.2.1: {}
|
||||
|
||||
terminal-link@2.1.1:
|
||||
dependencies:
|
||||
ansi-escapes: 4.3.2
|
||||
@@ -28619,8 +28304,6 @@ 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: {}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
#!/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.");
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/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).`);
|
||||
}
|
||||
+8
-1
@@ -6,7 +6,13 @@
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**"]
|
||||
"outputs": [".next/**", "!.next/cache/**", "dist/**", ".svelte-kit/**"]
|
||||
},
|
||||
"svelte-chat#build": {
|
||||
"dependsOn": ["^build"],
|
||||
"inputs": ["$TURBO_DEFAULT$", ".env*"],
|
||||
"outputs": [".svelte-kit/**"],
|
||||
"cache": false
|
||||
},
|
||||
"lint": {
|
||||
"dependsOn": ["^lint"]
|
||||
@@ -15,6 +21,7 @@
|
||||
"dependsOn": ["^build"]
|
||||
},
|
||||
"dev": {
|
||||
"dependsOn": ["^build"],
|
||||
"cache": false,
|
||||
"persistent": true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user