add solid example (#208)

This commit is contained in:
Chris Tate
2026-03-11 22:54:56 -05:00
committed by GitHub
parent c180f529a0
commit 316439fcd7
19 changed files with 683 additions and 13 deletions
+12
View File
@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>json-render solid example</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/index.tsx"></script>
</body>
</html>
+23
View File
@@ -0,0 +1,23 @@
{
"name": "example-solid",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "portless solid-demo.json-render vite",
"build": "vite build",
"preview": "vite preview",
"check-types": "tsc --noEmit"
},
"dependencies": {
"@json-render/core": "workspace:*",
"@json-render/solid": "workspace:*",
"solid-js": "^1.9.11",
"zod": "^4.3.6"
},
"devDependencies": {
"typescript": "^5.9.3",
"vite": "^7.3.1",
"vite-plugin-solid": "^2.11.10"
}
}
+13
View File
@@ -0,0 +1,13 @@
import { StateProvider } from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { DemoRenderer } from "./DemoRenderer";
export function App() {
const initialState = demoSpec.state ?? {};
return (
<StateProvider initialState={initialState}>
<DemoRenderer />
</StateProvider>
);
}
+51
View File
@@ -0,0 +1,51 @@
import {
ActionProvider,
ValidationProvider,
VisibilityProvider,
Renderer,
useStateStore,
} from "@json-render/solid";
import { demoSpec } from "./lib/spec";
import { registry } from "./lib/registry";
export function DemoRenderer() {
const stateStore = useStateStore();
const handlers = {
increment: async () => {
stateStore.set("/count", Number(stateStore.get("/count") || 0) + 1);
},
decrement: async () => {
stateStore.set(
"/count",
Math.max(0, Number(stateStore.get("/count") || 0) - 1),
);
},
reset: async () => {
stateStore.set("/count", 0);
},
toggleItem: async (params: Record<string, unknown>) => {
const index = params.index as number;
const todos = (
stateStore.get("/todos") as Array<{
id: number;
title: string;
completed: boolean;
}>
).map((item, i) =>
i === index ? { ...item, completed: !item.completed } : item,
);
stateStore.set("/todos", todos);
},
};
return (
<ActionProvider handlers={handlers}>
<VisibilityProvider>
<ValidationProvider>
<Renderer spec={demoSpec} registry={registry} />
</ValidationProvider>
</VisibilityProvider>
</ActionProvider>
);
}
+13
View File
@@ -0,0 +1,13 @@
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f9fafb;
color: #111827;
min-height: 100vh;
}
+5
View File
@@ -0,0 +1,5 @@
import { render } from "solid-js/web";
import { App } from "./App";
import "./app.css";
render(() => <App />, document.getElementById("app")!);
+90
View File
@@ -0,0 +1,90 @@
import { schema } from "@json-render/solid/schema";
import { z } from "zod";
export const catalog = schema.createCatalog({
components: {
Stack: {
props: z.object({
gap: z.number().optional(),
padding: z.number().optional(),
direction: z.enum(["vertical", "horizontal"]).optional(),
align: z.enum(["start", "center", "end"]).optional(),
}),
slots: ["default"],
description:
"Layout container that stacks children vertically or horizontally",
},
Card: {
props: z.object({
title: z.string().optional(),
subtitle: z.string().optional(),
}),
slots: ["default"],
description: "A card container with optional title and subtitle",
},
Text: {
props: z.object({
content: z.string(),
size: z.enum(["sm", "md", "lg", "xl"]).optional(),
weight: z.enum(["normal", "medium", "bold"]).optional(),
color: z.string().optional(),
}),
slots: [],
description: "Displays a text string",
},
Button: {
props: z.object({
label: z.string(),
variant: z.enum(["primary", "secondary", "danger"]).optional(),
disabled: z.boolean().optional(),
}),
slots: [],
description: "A clickable button that emits a 'press' event",
},
Badge: {
props: z.object({
label: z.string(),
color: z.string().optional(),
}),
slots: [],
description: "A small badge/tag label",
},
ListItem: {
props: z.object({
title: z.string(),
description: z.string().optional(),
completed: z.boolean().optional(),
}),
slots: [],
description: "A single item in a list",
},
Input: {
props: z.object({
value: z.string().optional(),
placeholder: z.string().optional(),
}),
slots: [],
description: "A text input field that supports two-way state binding",
},
},
actions: {
increment: {
params: z.object({}),
description: "Increment the counter by 1",
},
decrement: {
params: z.object({}),
description: "Decrement the counter by 1",
},
reset: {
params: z.object({}),
description: "Reset the counter to 0",
},
toggleItem: {
params: z.object({
index: z.number(),
}),
description: "Toggle the completed state of a todo item",
},
},
});
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface BadgeProps {
label: string;
color?: string;
}
export function Badge(renderProps: BaseComponentProps<BadgeProps>) {
const color = () => renderProps.props.color ?? "#6b7280";
return (
<span
style={{
display: "inline-block",
padding: "4px 12px",
"border-radius": "9999px",
"font-size": "13px",
"font-weight": "500",
"background-color": `${color()}20`,
color: color(),
border: `1px solid ${color()}40`,
}}
>
{renderProps.props.label}
</span>
);
}
@@ -0,0 +1,47 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ButtonProps {
label: string;
variant?: "primary" | "secondary" | "danger";
disabled?: boolean;
}
const variantStyles: Record<string, Record<string, string>> = {
primary: {
background: "#2563eb",
color: "#ffffff",
border: "1px solid #2563eb",
},
secondary: {
background: "#f3f4f6",
color: "#374151",
border: "1px solid #d1d5db",
},
danger: {
background: "#fee2e2",
color: "#dc2626",
border: "1px solid #fca5a5",
},
};
export function Button(renderProps: BaseComponentProps<ButtonProps>) {
const variant = () => renderProps.props.variant ?? "primary";
return (
<button
disabled={renderProps.props.disabled}
onClick={() => renderProps.emit("press")}
style={{
padding: "8px 16px",
"border-radius": "8px",
"font-size": "14px",
"font-weight": "500",
cursor: renderProps.props.disabled ? "not-allowed" : "pointer",
opacity: renderProps.props.disabled ? "0.5" : "1",
...variantStyles[variant()],
}}
>
{renderProps.props.label}
</button>
);
}
@@ -0,0 +1,44 @@
import type { BaseComponentProps } from "@json-render/solid";
interface CardProps {
title?: string;
subtitle?: string;
}
export function Card(renderProps: BaseComponentProps<CardProps>) {
return (
<div
style={{
background: "#ffffff",
"border-radius": "12px",
border: "1px solid #e5e7eb",
padding: "20px",
"box-shadow": "0 1px 3px rgba(0,0,0,0.06)",
}}
>
{renderProps.props.title && (
<h2
style={{
"font-size": "18px",
"font-weight": "600",
"margin-bottom": renderProps.props.subtitle ? "4px" : "16px",
}}
>
{renderProps.props.title}
</h2>
)}
{renderProps.props.subtitle && (
<p
style={{
"font-size": "14px",
color: "#6b7280",
"margin-bottom": "16px",
}}
>
{renderProps.props.subtitle}
</p>
)}
{renderProps.children}
</div>
);
}
@@ -0,0 +1,30 @@
import type { BaseComponentProps } from "@json-render/solid";
import { useBoundProp } from "@json-render/solid";
interface InputProps {
value?: string;
placeholder?: string;
}
export function Input(renderProps: BaseComponentProps<InputProps>) {
const [value, setValue] = useBoundProp<string>(
renderProps.props.value,
renderProps.bindings?.value,
);
return (
<input
value={String(value ?? "")}
onInput={(e) => setValue(e.currentTarget.value)}
placeholder={renderProps.props.placeholder ?? ""}
style={{
padding: "8px 12px",
"border-radius": "8px",
border: "1px solid #d1d5db",
"font-size": "14px",
outline: "none",
width: "100%",
}}
/>
);
}
@@ -0,0 +1,55 @@
import type { BaseComponentProps } from "@json-render/solid";
interface ListItemProps {
title: string;
description?: string;
completed?: boolean;
}
export function ListItem(renderProps: BaseComponentProps<ListItemProps>) {
return (
<div
onClick={() => renderProps.emit("press")}
style={{
display: "flex",
"align-items": "center",
gap: "10px",
padding: "10px 12px",
"border-radius": "8px",
border: "1px solid #e5e7eb",
cursor: "pointer",
"user-select": "none",
background: renderProps.props.completed ? "#f0fdf4" : "#ffffff",
}}
>
<span
style={{
width: "22px",
height: "22px",
"border-radius": "6px",
border: renderProps.props.completed
? "2px solid #22c55e"
: "2px solid #d1d5db",
display: "flex",
"align-items": "center",
"justify-content": "center",
"font-size": "13px",
color: "#22c55e",
"flex-shrink": "0",
}}
>
{renderProps.props.completed ? "\u2713" : ""}
</span>
<span
style={{
"text-decoration": renderProps.props.completed
? "line-through"
: "none",
color: renderProps.props.completed ? "#9ca3af" : "#111827",
}}
>
{renderProps.props.title}
</span>
</div>
);
}
@@ -0,0 +1,27 @@
import type { BaseComponentProps } from "@json-render/solid";
interface StackProps {
gap?: number;
padding?: number;
direction?: "vertical" | "horizontal";
align?: "start" | "center" | "end";
}
export function Stack(renderProps: BaseComponentProps<StackProps>) {
return (
<div
style={{
display: "flex",
"flex-direction":
renderProps.props.direction === "horizontal" ? "row" : "column",
gap: renderProps.props.gap ? `${renderProps.props.gap}px` : undefined,
padding: renderProps.props.padding
? `${renderProps.props.padding}px`
: undefined,
"align-items": renderProps.props.align,
}}
>
{renderProps.children}
</div>
);
}
@@ -0,0 +1,35 @@
import type { BaseComponentProps } from "@json-render/solid";
interface TextProps {
content: string;
size?: "sm" | "md" | "lg" | "xl";
weight?: "normal" | "medium" | "bold";
color?: string;
}
const sizeMap: Record<string, string> = {
sm: "12px",
md: "14px",
lg: "18px",
xl: "24px",
};
const weightMap: Record<string, string> = {
normal: "400",
medium: "500",
bold: "700",
};
export function Text(renderProps: BaseComponentProps<TextProps>) {
return (
<span
style={{
"font-size": sizeMap[renderProps.props.size ?? "md"],
"font-weight": weightMap[renderProps.props.weight ?? "normal"],
color: renderProps.props.color,
}}
>
{String(renderProps.props.content ?? "")}
</span>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { type Components, defineRegistry } from "@json-render/solid";
import { catalog } from "./catalog";
import { Stack } from "./components/Stack";
import { Card } from "./components/Card";
import { Text } from "./components/Text";
import { Button } from "./components/Button";
import { Badge } from "./components/Badge";
import { ListItem } from "./components/ListItem";
import { Input } from "./components/Input";
const components: Components<typeof catalog> = {
Stack,
Card,
Text,
Button,
Badge,
ListItem,
Input,
};
export const { registry, handlers: makeHandlers } = defineRegistry(catalog, {
components,
actions: {
increment: async () => {},
decrement: async () => {},
reset: async () => {},
toggleItem: async () => {},
},
});
+130
View File
@@ -0,0 +1,130 @@
import type { Spec } from "@json-render/core";
export const demoSpec: Spec = {
root: "root",
state: {
count: 0,
name: "",
todos: [
{ id: 1, title: "Learn SolidJS", completed: true },
{ id: 2, title: "Try @json-render/solid", completed: false },
{ id: 3, title: "Build something awesome", completed: false },
],
},
elements: {
root: {
type: "Stack",
props: { gap: 24, padding: 24, direction: "vertical" },
children: [
"header",
"counter-card",
"milestone-badge",
"todos-card",
"input-card",
],
},
header: {
type: "Text",
props: {
content: "@json-render/solid demo",
size: "xl",
weight: "bold",
},
},
"counter-card": {
type: "Card",
props: {
title: "Counter",
subtitle: "Click the buttons to change the count",
},
children: ["counter-body"],
},
"counter-body": {
type: "Stack",
props: { gap: 12, direction: "horizontal", align: "center" },
children: [
"decrement-btn",
"counter-value",
"increment-btn",
"reset-btn",
],
},
"decrement-btn": {
type: "Button",
props: { label: "\u2212", variant: "secondary" },
on: { press: { action: "decrement" } },
},
"counter-value": {
type: "Text",
props: {
content: { $state: "/count" },
size: "xl",
weight: "bold",
},
},
"increment-btn": {
type: "Button",
props: { label: "+", variant: "primary" },
on: { press: { action: "increment" } },
},
"reset-btn": {
type: "Button",
props: { label: "Reset", variant: "danger" },
on: { press: { action: "reset" } },
},
"milestone-badge": {
type: "Badge",
props: { label: "Milestone reached: 10!", color: "#10b981" },
visible: { $state: "/count", gte: 10 },
},
"todos-card": {
type: "Card",
props: { title: "Todo List", subtitle: "Your tasks" },
children: ["todos-list"],
},
"todos-list": {
type: "Stack",
props: { gap: 8, direction: "vertical" },
repeat: { statePath: "/todos", key: "id" },
children: ["todo-item"],
},
"todo-item": {
type: "ListItem",
props: {
title: { $item: "title" },
completed: { $item: "completed" },
},
on: {
press: { action: "toggleItem", params: { index: { $index: true } } },
},
},
"input-card": {
type: "Card",
props: {
title: "Bound Input",
subtitle: "Type to update state and see reactive text",
},
children: ["input-body"],
},
"input-body": {
type: "Stack",
props: { gap: 12, direction: "vertical" },
children: ["name-input", "name-display"],
},
"name-input": {
type: "Input",
props: {
value: { $bindState: "/name" },
placeholder: "Enter your name...",
},
},
"name-display": {
type: "Text",
props: {
content: { $state: "/name" },
size: "md",
color: "#6b7280",
},
},
},
};
+18
View File
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"strict": true,
"skipLibCheck": true,
"jsx": "preserve",
"jsxImportSource": "solid-js"
},
"include": ["src/**/*.ts", "src/**/*.tsx"],
"exclude": ["node_modules"]
}
+6
View File
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import solid from "vite-plugin-solid";
export default defineConfig({
plugins: [solid()],
});
+28 -13
View File
@@ -946,6 +946,31 @@ importers:
specifier: ^5.7.2
version: 5.9.3
examples/solid:
dependencies:
'@json-render/core':
specifier: workspace:*
version: link:../../packages/core
'@json-render/solid':
specifier: workspace:*
version: link:../../packages/solid
solid-js:
specifier: ^1.9.11
version: 1.9.11
zod:
specifier: ^4.3.6
version: 4.3.6
devDependencies:
typescript:
specifier: ^5.9.3
version: 5.9.3
vite:
specifier: ^7.3.1
version: 7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
vite-plugin-solid:
specifier: ^2.11.10
version: 2.11.10(solid-js@1.9.11)(vite@7.3.1(@types/node@22.19.6)(jiti@2.6.1)(lightningcss@1.31.1)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2))
examples/stripe-app/api:
dependencies:
'@ai-sdk/gateway':
@@ -14354,7 +14379,6 @@ snapshots:
- graphql
- supports-color
- utf-8-validate
optional: true
'@expo/code-signing-certificates@0.0.6':
dependencies:
@@ -14419,7 +14443,6 @@ snapshots:
optionalDependencies:
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
'@expo/env@2.0.8':
dependencies:
@@ -14489,7 +14512,7 @@ snapshots:
postcss: 8.4.49
resolve-from: 5.0.0
optionalDependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -14570,7 +14593,7 @@ snapshots:
'@expo/json-file': 10.0.8
'@react-native/normalize-colors': 0.81.5
debug: 4.4.3
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
resolve-from: 5.0.0
semver: 7.7.3
xml2js: 0.6.0
@@ -14598,7 +14621,6 @@ snapshots:
expo-font: 14.0.11(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)
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
'@expo/ws-tunnel@1.0.6': {}
@@ -19483,7 +19505,7 @@ snapshots:
resolve-from: 5.0.0
optionalDependencies:
'@babel/runtime': 7.28.6
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0)
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
transitivePeerDependencies:
- '@babel/core'
- supports-color
@@ -20916,7 +20938,6 @@ snapshots:
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
transitivePeerDependencies:
- supports-color
optional: true
expo-clipboard@8.0.8(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -20941,7 +20962,6 @@ snapshots:
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
transitivePeerDependencies:
- supports-color
optional: true
expo-file-system@19.0.21(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0)):
dependencies:
@@ -20952,7 +20972,6 @@ snapshots:
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@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)
optional: true
expo-font@14.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -20967,7 +20986,6 @@ snapshots:
fontfaceobserver: 2.3.0
react: 19.2.4
react-native: 0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4)
optional: true
expo-keep-awake@15.0.8(expo@54.0.33)(react@19.1.0):
dependencies:
@@ -20978,7 +20996,6 @@ snapshots:
dependencies:
expo: 54.0.33(@babel/core@7.29.0)(@expo/metro-runtime@6.1.2)(expo-router@6.0.23)(graphql@16.12.0)(react-native@0.83.1(@babel/core@7.29.0)(@types/react@19.2.3)(react@19.2.4))(react@19.2.4)
react: 19.2.4
optional: true
expo-linking@8.0.11(expo@54.0.33)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -21020,7 +21037,6 @@ snapshots:
invariant: 2.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)
optional: true
expo-router@6.0.23(@expo/metro-runtime@6.1.2)(@types/react-dom@19.2.3(@types/react@19.1.17))(@types/react@19.1.17)(expo-constants@18.0.13)(expo-linking@8.0.11)(expo@54.0.33)(react-dom@19.2.4(react@19.1.0))(react-native-reanimated@4.2.1(react-native-worklets@0.7.2(@babel/core@7.29.0)(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-safe-area-context@5.4.0(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native-screens@4.11.1(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0))(react-native@0.81.4(@babel/core@7.29.0)(@types/react@19.1.17)(react@19.1.0))(react@19.1.0):
dependencies:
@@ -21183,7 +21199,6 @@ snapshots:
- graphql
- supports-color
- utf-8-validate
optional: true
exponential-backoff@3.1.3: {}