Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/app/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,11 @@ body {
padding-top: 4rem !important;
}
}

.panel-empty {
display: flex;
align-items: center;
justify-content: center;
color: var(--openui-text-neutral-secondary);
height: 100vh;
}
17 changes: 17 additions & 0 deletions docs/app/paste/components/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client";

import { Callout } from "@openuidev/react-ui";

export function Banner({
tone,
children,
}: {
tone: "warning" | "danger" | "info";
children: React.ReactNode;
}) {
return (
<div className="banner-slot">
<Callout variant={tone} description={children} />
</div>
);
}
76 changes: 76 additions & 0 deletions docs/app/paste/components/EditorPane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"use client";

import { EditorView } from "@codemirror/view";
import CodeMirror from "@uiw/react-codemirror";
import { useMemo } from "react";
import { useTheme } from "next-themes";
import { componentHoverTooltip, type ComponentDefs } from "@paste/lib/editor/hover";
import { openuiLang } from "@paste/lib/editor/openui-lang";

const editorTheme = EditorView.theme({
"&": {
height: "100%",
fontSize: "14px",
backgroundColor: "transparent",
},
".cm-content": {
fontFamily: "var(--openui-font-code, ui-monospace, SFMono-Regular, monospace)",
caretColor: "var(--openui-text-neutral-primary, currentColor)",
},
".cm-gutters": {
backgroundColor: "transparent",
color: "var(--openui-text-neutral-tertiary, #888)",
border: "none",
},
"&.cm-focused": { outline: "none" },
".cm-activeLine": { backgroundColor: "var(--openui-sunk, rgba(0,0,0,0.03))" },
".cm-activeLineGutter": { backgroundColor: "transparent" },
});

export function EditorPane({
code,
onChange,
readOnly,
componentDefs,
}: {
code: string;
onChange: (code: string) => void;
readOnly: boolean;
/** Active library's schema $defs — powers hover signatures. */
componentDefs: ComponentDefs | null;
}) {
const extensions = useMemo(
() => [
openuiLang,
editorTheme,
EditorView.lineWrapping,
componentHoverTooltip(() => componentDefs),
],
[componentDefs],
);
// Docs theming is class-based via next-themes, not prefers-color-scheme.
const dark = useTheme().resolvedTheme === "dark";
return (
<div className="editor-pane">
<CodeMirror
value={code}
onChange={onChange}
extensions={extensions}
// Built-in dark theme keeps syntax colors readable on the dark token
// background; our editorTheme keeps the background itself transparent.
theme={dark ? "dark" : "light"}
readOnly={readOnly}
height="100%"
style={{ height: "100%" }}
basicSetup={{
lineNumbers: true,
foldGutter: false,
highlightActiveLine: true,
autocompletion: false,
}}
placeholder={'Paste OpenUI Lang code, e.g.\n\nroot = Stack([title])\ntitle = TextContent("Hello")'}
/>
{readOnly && <div className="editor-lock">read-only during playback</div>}
</div>
);
}
110 changes: 110 additions & 0 deletions docs/app/paste/components/HelpDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
"use client";

import { Button } from "@openuidev/react-ui";
import { HelpCircle, X } from "lucide-react";
import { useEffect, useState } from "react";

export function HelpDialog() {
const [open, setOpen] = useState(false);

useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") setOpen(false);
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [open]);

return (
<>
<Button
variant="secondary"
size="small"
iconLeft={<HelpCircle size={14} />}
onClick={() => setOpen(true)}
aria-haspopup="dialog"
>
Help
</Button>
{open && (
<div className="help-overlay" onClick={() => setOpen(false)}>
<div
className="help-dialog"
role="dialog"
aria-modal="true"
aria-label="How to use paste"
onClick={(e) => e.stopPropagation()}
>
<div className="help-header">
<h2>How to use paste</h2>
<Button
variant="tertiary"
size="small"
iconLeft={<X size={14} />}
onClick={() => setOpen(false)}
aria-label="Close"
/>
</div>

<div className="help-body">
<p>
paste is a playground for <strong>OpenUI Lang</strong>, the DSL that LLMs emit to
generate UI. Paste code on the left; the panels on the right show what the parser
and renderer make of it.
</p>

<h3>Quick start</h3>
<ol>
<li>
Pick an example from <strong>Examples</strong>, or paste your own Lang code
(statements look like <code>root = Stack([title])</code>).
</li>
<li>
Validation runs as you type against the selected <strong>lang-core</strong>{" "}
version. Every published npm version is available and loaded on demand.
</li>
<li>
Switch the component <strong>Library</strong> (openui, openui chat) to validate and render against a different component set.
</li>
</ol>

<h3>Panels</h3>
<ul>
<li>
<strong>Render</strong>: live output. Interactive: forms hold state, and{" "}
<code>Query()</code> calls resolve with mocked data. Always rendered with the
bundled renderer; the selected lang-core version drives the other panels.
</li>
<li>
<strong>Validation</strong>: errors grouped by code, with fix hints, plus
unresolved refs and orphaned statements.
</li>
<li>
<strong>Tree</strong>: the parsed element tree, state declarations, queries and
mutations.
</li>
<li>
<strong>JSON</strong>: the raw ParseResult.
</li>
<li>
<strong>Stream</strong>: a per-chunk trace of the streaming parse with a final
streaming-vs-one-shot convergence check.
</li>
</ul>

<h3>Streaming mode</h3>
<p>
<strong>Stream</strong> replays your code as if an LLM were emitting it.{" "}
<em>LLM-like</em> chunking adds realistic jitter and stalls (reproducible via{" "}
<strong>Seed</strong>). Watch the render window build up live, pause and{" "}
<strong>Step</strong> chunk by chunk, and check whether the root ever appears or
drops mid-stream.
</p>
</div>
</div>
</div>
)}
</>
);
}
Loading
Loading