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:
2026-02-28 00:56:59 +00:00
parent af85ad3b07
commit 968faac7f5
8 changed files with 250 additions and 85 deletions

View File

@@ -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 &rarr; 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>