mirror of
https://github.com/kennethnym/jrx.git
synced 2026-03-20 03:41:18 +00:00
Fix counter, add input binding, syntax highlighting, dark mode
- Fix increment button: custom action handler instead of no-op setState - Toggle visibility on Show/Hide Details button via $cond - Input uses $bindState + useBoundProp for two-way binding - Add shiki syntax highlighting (catppuccin-latte/mocha dual theme) - Dark mode via prefers-color-scheme with CSS variables - Layout: Live UI left, JSX Source + JSON Output stacked right Co-authored-by: Ona <no-reply@ona.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useRef } from "react";
|
||||
import { Renderer, StateProvider, ActionProvider, VisibilityProvider } from "@json-render/react";
|
||||
import type { Spec } from "@json-render/core";
|
||||
import { registry, handlers } from "./registry";
|
||||
import { useHighlight } from "./useHighlight";
|
||||
|
||||
const SPECS = ["simple", "full"] as const;
|
||||
|
||||
@@ -30,6 +31,22 @@ function SpecRenderer({ spec }: { spec: Spec }) {
|
||||
);
|
||||
}
|
||||
|
||||
function CodeBlock({ html, fallback, maxHeight = "40vh" }: { html: string; fallback: string; maxHeight?: string }) {
|
||||
if (html) {
|
||||
return (
|
||||
<div
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
style={{ borderRadius: "8px", overflow: "auto", maxHeight, fontSize: "13px", lineHeight: "1.5" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<pre style={{ backgroundColor: "var(--code-bg)", color: "var(--code-fg)", padding: "16px", borderRadius: "8px", fontSize: "13px", lineHeight: "1.5", overflow: "auto", maxHeight, margin: 0 }}>
|
||||
{fallback}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
|
||||
export function App() {
|
||||
const [activeSpec, setActiveSpec] = useState<string | null>(null);
|
||||
const [spec, setSpec] = useState<Spec | null>(null);
|
||||
@@ -37,6 +54,9 @@ export function App() {
|
||||
const [jsxSource, setJsxSource] = useState<string>("");
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const jsxHtml = useHighlight(jsxSource, "tsx");
|
||||
const jsonHtml = useHighlight(specJson, "json");
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeSpec) return;
|
||||
setLoading(true);
|
||||
@@ -56,7 +76,7 @@ export function App() {
|
||||
return (
|
||||
<div style={{ maxWidth: 600, margin: "60px auto", padding: "0 24px" }}>
|
||||
<h1 style={{ fontSize: "24px", marginBottom: "8px" }}>jrx examples</h1>
|
||||
<p style={{ color: "#666", fontSize: "14px", marginBottom: "24px" }}>
|
||||
<p style={{ color: "var(--text-secondary)", fontSize: "14px", marginBottom: "24px" }}>
|
||||
JSX → json-render Spec. Pick a spec to see the live UI and JSON
|
||||
output.
|
||||
</p>
|
||||
@@ -68,15 +88,16 @@ export function App() {
|
||||
style={{
|
||||
padding: "12px 16px",
|
||||
fontSize: "15px",
|
||||
backgroundColor: "#fff",
|
||||
border: "1px solid #d0d0d0",
|
||||
backgroundColor: "var(--bg-surface)",
|
||||
color: "var(--text)",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "8px",
|
||||
cursor: "pointer",
|
||||
textAlign: "left",
|
||||
}}
|
||||
>
|
||||
<strong>{name}</strong>
|
||||
<span style={{ color: "#888", marginLeft: "8px" }}>
|
||||
<span style={{ color: "var(--text-muted)", marginLeft: "8px" }}>
|
||||
{name === "simple"
|
||||
? "Flat elements, no state"
|
||||
: "Nested layout with state, events, visibility, watchers"}
|
||||
@@ -93,8 +114,8 @@ export function App() {
|
||||
<div
|
||||
style={{
|
||||
padding: "12px 24px",
|
||||
borderBottom: "1px solid #e0e0e0",
|
||||
backgroundColor: "#fff",
|
||||
borderBottom: "1px solid var(--border-light)",
|
||||
backgroundColor: "var(--bg-surface)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "16px",
|
||||
@@ -108,7 +129,7 @@ export function App() {
|
||||
style={{
|
||||
background: "none",
|
||||
border: "none",
|
||||
color: "#4f46e5",
|
||||
color: "var(--accent)",
|
||||
cursor: "pointer",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
@@ -119,69 +140,39 @@ export function App() {
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p style={{ padding: "24px", color: "#888" }}>Loading...</p>
|
||||
<p style={{ padding: "24px", color: "var(--text-muted)" }}>Loading...</p>
|
||||
) : (
|
||||
spec && (
|
||||
<div style={{ padding: "24px", display: "flex", flexDirection: "column", gap: "24px" }}>
|
||||
{/* JSX Source */}
|
||||
<div>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px", color: "#111" }}>
|
||||
JSX Source
|
||||
<div style={{ display: "flex", gap: "24px", padding: "24px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px" }}>
|
||||
Live UI
|
||||
</h2>
|
||||
<pre
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "#1e1e2e",
|
||||
color: "#cdd6f4",
|
||||
padding: "16px",
|
||||
border: "1px solid var(--border-light)",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.5",
|
||||
overflow: "auto",
|
||||
maxHeight: "40vh",
|
||||
margin: 0,
|
||||
padding: "16px",
|
||||
backgroundColor: "var(--bg-surface)",
|
||||
}}
|
||||
>
|
||||
{jsxSource}
|
||||
</pre>
|
||||
<SpecRenderer spec={spec} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live UI + JSON side by side */}
|
||||
<div style={{ display: "flex", gap: "24px", alignItems: "flex-start" }}>
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px", color: "#111" }}>
|
||||
Live UI
|
||||
<div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: "24px" }}>
|
||||
<div>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px" }}>
|
||||
JSX Source
|
||||
</h2>
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #e0e0e0",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
backgroundColor: "#fff",
|
||||
}}
|
||||
>
|
||||
<SpecRenderer spec={spec} />
|
||||
</div>
|
||||
<CodeBlock html={jsxHtml} fallback={jsxSource} />
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px", color: "#111" }}>
|
||||
<div>
|
||||
<h2 style={{ margin: "0 0 12px 0", fontSize: "16px" }}>
|
||||
JSON Output
|
||||
</h2>
|
||||
<pre
|
||||
style={{
|
||||
backgroundColor: "#1e1e2e",
|
||||
color: "#cdd6f4",
|
||||
padding: "16px",
|
||||
borderRadius: "8px",
|
||||
fontSize: "13px",
|
||||
lineHeight: "1.5",
|
||||
overflow: "auto",
|
||||
maxHeight: "80vh",
|
||||
margin: 0,
|
||||
}}
|
||||
>
|
||||
{specJson}
|
||||
</pre>
|
||||
<CodeBlock html={jsonHtml} fallback={specJson} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -34,8 +34,9 @@ export const catalog = defineCatalog(schema, {
|
||||
Input: {
|
||||
props: z.object({
|
||||
placeholder: z.string().nullable(),
|
||||
value: z.string().nullable(),
|
||||
}),
|
||||
description: "Text input field",
|
||||
description: "Text input field. Use { $bindState } on value for two-way binding.",
|
||||
},
|
||||
},
|
||||
actions: {
|
||||
|
||||
@@ -5,8 +5,50 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>jrx example</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f5f5f5;
|
||||
--bg-surface: #fff;
|
||||
--bg-surface-alt: #fafafa;
|
||||
--text: #111;
|
||||
--text-secondary: #666;
|
||||
--text-muted: #888;
|
||||
--text-body: #444;
|
||||
--border: #d0d0d0;
|
||||
--border-light: #e0e0e0;
|
||||
--accent: #4f46e5;
|
||||
--code-bg: #1e1e2e;
|
||||
--code-fg: #cdd6f4;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #1a1a2e;
|
||||
--bg-surface: #232340;
|
||||
--bg-surface-alt: #2a2a4a;
|
||||
--text: #e0e0e0;
|
||||
--text-secondary: #a0a0b0;
|
||||
--text-muted: #808090;
|
||||
--text-body: #c0c0d0;
|
||||
--border: #3a3a5a;
|
||||
--border-light: #333355;
|
||||
--accent: #818cf8;
|
||||
--code-bg: #11111b;
|
||||
--code-fg: #cdd6f4;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; background: #f5f5f5; color: #111; }
|
||||
body { font-family: system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); }
|
||||
|
||||
/* Shiki dual theme */
|
||||
.shiki,
|
||||
.shiki span { color: var(--shiki-light); background-color: var(--shiki-light-bg); }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.shiki,
|
||||
.shiki span { color: var(--shiki-dark) !important; background-color: var(--shiki-dark-bg) !important; }
|
||||
}
|
||||
|
||||
/* Code block padding */
|
||||
.shiki { padding: 16px; border-radius: 8px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineRegistry } from "@json-render/react";
|
||||
import { defineRegistry, useBoundProp } from "@json-render/react";
|
||||
import { catalog } from "./catalog";
|
||||
|
||||
export const { registry, handlers } = defineRegistry(catalog, {
|
||||
@@ -19,14 +19,14 @@ export const { registry, handlers } = defineRegistry(catalog, {
|
||||
Card: ({ props, children }) => (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid #d0d0d0",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "8px",
|
||||
padding: "16px",
|
||||
backgroundColor: "#fafafa",
|
||||
backgroundColor: "var(--bg-surface-alt)",
|
||||
}}
|
||||
>
|
||||
{props.title && (
|
||||
<h3 style={{ margin: "0 0 12px 0", fontSize: "16px", color: "#333" }}>
|
||||
<h3 style={{ margin: "0 0 12px 0", fontSize: "16px" }}>
|
||||
{props.title}
|
||||
</h3>
|
||||
)}
|
||||
@@ -37,7 +37,7 @@ export const { registry, handlers } = defineRegistry(catalog, {
|
||||
),
|
||||
|
||||
Text: ({ props }) => (
|
||||
<span style={{ fontSize: "14px", color: "#444" }}>
|
||||
<span style={{ fontSize: "14px", color: "var(--text-body)" }}>
|
||||
{String(props.content ?? "")}
|
||||
</span>
|
||||
),
|
||||
@@ -48,7 +48,7 @@ export const { registry, handlers } = defineRegistry(catalog, {
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
fontSize: "14px",
|
||||
backgroundColor: "#4f46e5",
|
||||
backgroundColor: "var(--accent)",
|
||||
color: "#fff",
|
||||
border: "none",
|
||||
borderRadius: "6px",
|
||||
@@ -60,19 +60,25 @@ export const { registry, handlers } = defineRegistry(catalog, {
|
||||
</button>
|
||||
),
|
||||
|
||||
Input: ({ props, emit }) => (
|
||||
<input
|
||||
placeholder={props.placeholder ?? ""}
|
||||
onChange={() => emit("change")}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
fontSize: "14px",
|
||||
border: "1px solid #d0d0d0",
|
||||
borderRadius: "6px",
|
||||
outline: "none",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
Input: ({ props, bindings }) => {
|
||||
const [value, setValue] = useBoundProp<string>(props.value, bindings?.value);
|
||||
return (
|
||||
<input
|
||||
placeholder={props.placeholder ?? ""}
|
||||
value={value ?? ""}
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
style={{
|
||||
padding: "8px 12px",
|
||||
fontSize: "14px",
|
||||
border: "1px solid var(--border)",
|
||||
borderRadius: "6px",
|
||||
outline: "none",
|
||||
backgroundColor: "var(--bg-surface)",
|
||||
color: "var(--text)",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
actions: {
|
||||
|
||||
40
example/src/useHighlight.ts
Normal file
40
example/src/useHighlight.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { createHighlighter, type Highlighter } from "shiki";
|
||||
|
||||
let highlighterPromise: Promise<Highlighter> | null = null;
|
||||
|
||||
function getHighlighter() {
|
||||
if (!highlighterPromise) {
|
||||
highlighterPromise = createHighlighter({
|
||||
themes: ["catppuccin-mocha", "catppuccin-latte"],
|
||||
langs: ["tsx", "json"],
|
||||
});
|
||||
}
|
||||
return highlighterPromise;
|
||||
}
|
||||
|
||||
export function useHighlight(code: string, lang: "tsx" | "json"): string {
|
||||
const [html, setHtml] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (!code) return;
|
||||
let cancelled = false;
|
||||
getHighlighter().then((highlighter) => {
|
||||
if (cancelled) return;
|
||||
setHtml(
|
||||
highlighter.codeToHtml(code, {
|
||||
lang,
|
||||
themes: {
|
||||
light: "catppuccin-latte",
|
||||
dark: "catppuccin-mocha",
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [code, lang]);
|
||||
|
||||
return html;
|
||||
}
|
||||
Reference in New Issue
Block a user