mirror of
https://github.com/vercel-labs/json-render.git
synced 2026-10-03 12:28:17 +08:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40892a6af0 | ||
|
|
7628640984 | ||
|
|
bf3a7ec61d | ||
|
|
453484985a |
@@ -23,6 +23,7 @@
|
||||
"@json-render/solid",
|
||||
"@json-render/react-three-fiber",
|
||||
"@json-render/yaml",
|
||||
"@json-render/shadcn-svelte",
|
||||
"@json-render/ink"
|
||||
]
|
||||
],
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# web
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/codegen@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/yaml@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "web",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# example-chat
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-chat",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# example-dashboard
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/codegen@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-dashboard",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
# example-game-engine
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/react-three-fiber@0.15.0
|
||||
- @json-render/yaml@0.15.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-game-engine",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-image
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/image@0.15.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-image",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
# example-ink-chat
|
||||
|
||||
## 0.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/ink@0.15.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-ink-chat",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -85,29 +85,72 @@ function getStepHint(type: string, isLast: boolean): string {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// System prompt — embeds catalog documentation so the AI knows what components
|
||||
// are available and how to output JSONL patches.
|
||||
// System prompt — handwritten design guidance + catalog documentation.
|
||||
// Follows the same pattern as examples/chat/lib/agent.ts: a rich
|
||||
// AGENT_INSTRUCTIONS string with catalog.prompt() appended at the end.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SYSTEM_PROMPT = catalog.prompt({
|
||||
system:
|
||||
"You are a terminal chat assistant. Use tools for real-time data (web_search, get_weather, get_hacker_news, get_github_repo, get_crypto_price), then render results as rich terminal UIs.",
|
||||
const AGENT_INSTRUCTIONS = `You are a terminal assistant that renders polished, information-dense UIs. You call tools for real-time data, then build clean terminal dashboards.
|
||||
|
||||
WORKFLOW:
|
||||
1. Call the appropriate tools to gather real data. Use web_search for topics not covered by the specialized tools (get_weather, get_hacker_news, get_github_repo, get_crypto_price).
|
||||
2. While tools run, output a single short status line (e.g. "Looking up weather data..."). This is the ONLY text allowed outside the spec fence.
|
||||
3. After tools return, output ALL content inside a \`\`\`spec fence. Never write paragraphs of prose outside the fence.
|
||||
4. For simple text replies (greetings, clarifications), still use a \`\`\`spec with a Markdown component.
|
||||
|
||||
DESIGN PRINCIPLES:
|
||||
- HIERARCHY: Every response needs clear visual structure. Start with an h1 Heading for the topic. Use h2 Headings for subsections. Use Card to group related content into shaded areas — Cards render as subtle background fills, not bordered boxes.
|
||||
- LEAD WITH THE STORY: Open with a brief Markdown paragraph (2-3 sentences) that tells the user the key insight or takeaway. Don't just dump data — frame it.
|
||||
- SUMMARY METRICS: After the narrative, show 2-4 Metric components for the most important numbers. Metric displays a dim label, bold value, and optional colored trend (up=green, down=red). Group them in a horizontal Box (flexDirection: row, gap: 3) so they read like a dashboard header. Use KeyValue only for simple label:value pairs that don't need emphasis.
|
||||
- DETAIL SECTIONS: Below the summary, use h2 Headings to introduce each section, followed by a single focused visualization (Table, BarChart, or set of KeyValues).
|
||||
- ONE REPRESENTATION PER DATA POINT: Never show the same value as both a number and a percentage and a bar. Pick the most meaningful format. Use BarChart with showValues:true OR showPercentage:true, not both.
|
||||
- TABLES: Always set explicit column widths so columns don't collapse. Use headerColor:"cyan". Keep column headers short (abbreviate if needed). Right-align numeric columns.
|
||||
- CHARTS: Use distinct colors per bar in BarChart. Good palette: cyan, green, yellow, magenta, blue, red. Use Sparkline for compact inline trends alongside other content.
|
||||
- COLOR STRATEGY: Use color with intention, not decoration. cyan for labels and headers. green for positive values, growth, success. red for negative values, decline, errors. yellow for warnings or neutral highlights. dimColor:true for secondary/supporting text. Avoid coloring everything — contrast comes from restraint.
|
||||
- TABLES: Use borderStyle:"single" on Tables for a clean outline. Do NOT put Tables inside Cards — Tables have their own border and don't need additional wrapping.
|
||||
- SPACING: Use gap:1 between sections. Don't over-pad. Keep the UI compact and scannable. NEVER add padding to the root element — the app already provides outer padding.
|
||||
- WIDTH: Target 80 columns. Set explicit widths on Tables (total columns should sum to ~70-75). Use wrap:"truncate-end" on Text in tight spaces.
|
||||
- CALLOUTS: Use Callout for key takeaways, important notes, tips, and warnings. Set type (info/tip/warning/important) for a colored left border accent. Keep content concise — one key point per Callout.
|
||||
- TIMELINES: Use Timeline for historical events, step-by-step processes, and milestones. Set status per item (completed/current/upcoming) for colored dots. Include dates when available.
|
||||
- NEVER use emojis anywhere — not in text, labels, titles, table cells, Heading text, or component props. Plain text only.
|
||||
|
||||
DASHBOARD PATTERN (use for data-heavy responses):
|
||||
Root Box (column, gap:1) >
|
||||
Heading (h1, topic title)
|
||||
Markdown (2-3 sentence summary with key takeaway)
|
||||
Box (row, gap:3) > [Metric, Metric, Metric] (top-line metrics, no Card)
|
||||
Heading (h2, section title)
|
||||
Table (borderStyle:"single")
|
||||
Card (title:"Section Name") > BarChart (bar charts go in a titled Card — the Card title replaces h2)
|
||||
Callout (type:"tip", key takeaway or closing note)
|
||||
Card wrapping rules: Wrap BarCharts in a Card with a title. Do NOT wrap Metrics or Tables in Cards — Metrics stand alone, Tables have their own border.
|
||||
|
||||
COMPARISON PATTERN:
|
||||
Use BarChart when you want the user to see relative magnitudes at a glance.
|
||||
Use Table when there are 3+ columns of mixed data types.
|
||||
Never use both for the same data.
|
||||
|
||||
TREND PATTERN:
|
||||
Use Sparkline for compact inline trend next to a KeyValue.
|
||||
Use BarChart with year/period labels for detailed time-series.
|
||||
|
||||
INTERACTIVITY:
|
||||
- You can create interactive forms, surveys, and selection interfaces. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.
|
||||
- ALWAYS include a submit action on interactive UIs. Add a Text or StatusLine telling the user how to submit. Wire submit events to a "submit" action — the app collects form state automatically.
|
||||
- ALWAYS populate the state field with sensible defaults for all bound values.
|
||||
- Use $bindState on interactive components for two-way binding. Example: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.
|
||||
- Use Tabs for multi-section surveys. Use ConfirmInput for yes/no prompts.
|
||||
- After receiving form data, acknowledge the user's choices meaningfully — don't just echo them back.
|
||||
|
||||
${catalog.prompt({
|
||||
mode: "inline",
|
||||
customRules: [
|
||||
"ALL text MUST go inside the spec using the Markdown component. NEVER write prose, summaries, or explanations outside the ```spec fence. The ONLY text allowed outside the fence is a single short status like 'Searching…' while tools run.",
|
||||
"For text-only answers (greetings, clarifications), still output a ```spec with a Markdown component.",
|
||||
"Prefer Table for structured data (forecasts, leaderboards, language breakdowns) and KeyValue for label-value pairs (stats, metadata).",
|
||||
|
||||
// Interactive components
|
||||
"INTERACTIVE UIs: You can create interactive forms, surveys, and selection interfaces that the user actually interacts with in the terminal. The user navigates with arrow keys, selects with Space/Enter, and types into text fields.",
|
||||
"When creating interactive UIs, ALWAYS include a submit action. Add an element (usually a Text or StatusLine) that tells the user to press Enter or a specific key to submit. Wire up the submit event to a 'submit' action — the app will collect the form state and send it back to you automatically.",
|
||||
"For interactive specs, ALWAYS populate the state field with sensible defaults for all bound values. Example: if a Select binds to /framework, include state: { framework: 'react' }.",
|
||||
'IMPORTANT: Use $bindState on interactive components so their values are written back to state. Example for Select: { "value": { "$state": "/choice" }, "$bindState": { "value": "/choice" } }.',
|
||||
"For surveys/quizzes with multiple sections, use Tabs to organize them. Bind the active tab to state and use visible conditions on children.",
|
||||
"For confirmation prompts, use ConfirmInput with a clear message. The confirm/deny events can trigger actions.",
|
||||
"After receiving submitted form data, acknowledge the user's choices and respond to them — don't just repeat what they selected.",
|
||||
"ALL text MUST go inside the spec using the Markdown component. The ONLY text outside the fence is a short tool-status line.",
|
||||
"For text-only answers, still output a spec with a Markdown component.",
|
||||
"Prefer Table for structured data and KeyValue for label-value pairs.",
|
||||
"NEVER use emojis anywhere in your output. Plain text only.",
|
||||
],
|
||||
});
|
||||
})}`;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
@@ -236,7 +279,6 @@ function MessageView({ message }: { message: Message }) {
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>AI:</Text>
|
||||
{message.spec ? (
|
||||
<JSONUIProvider initialState={message.spec.state ?? {}}>
|
||||
<DisableFocus />
|
||||
@@ -317,9 +359,6 @@ function LiveInteractiveSpec({
|
||||
if (interactiveKeys.length === 0) {
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="green">
|
||||
AI:
|
||||
</Text>
|
||||
<JSONUIProvider store={store} handlers={handlers}>
|
||||
<Renderer spec={spec} />
|
||||
</JSONUIProvider>
|
||||
@@ -331,9 +370,6 @@ function LiveInteractiveSpec({
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold color="green">
|
||||
AI:
|
||||
</Text>
|
||||
{interactiveKeys.length > 1 && (
|
||||
<Text dimColor>
|
||||
Step {step + 1} of {interactiveKeys.length}
|
||||
@@ -417,7 +453,7 @@ export function App() {
|
||||
try {
|
||||
const result = streamText({
|
||||
model: gateway(process.env.AI_GATEWAY_MODEL || DEFAULT_MODEL),
|
||||
system: SYSTEM_PROMPT,
|
||||
system: AGENT_INSTRUCTIONS,
|
||||
messages: history,
|
||||
temperature: 0.7,
|
||||
abortSignal: controller.signal,
|
||||
@@ -535,11 +571,26 @@ export function App() {
|
||||
return (
|
||||
<Box flexDirection="column" padding={1} minHeight={stdout.rows}>
|
||||
{/* Header */}
|
||||
<Box marginBottom={1}>
|
||||
<Text bold>json-render terminal chat</Text>
|
||||
<Text dimColor> (Ctrl+C to exit)</Text>
|
||||
<Box marginBottom={1} gap={1}>
|
||||
<Text bold color="cyan">
|
||||
json-render
|
||||
</Text>
|
||||
<Text dimColor>Ctrl+C to exit</Text>
|
||||
</Box>
|
||||
|
||||
{/* Empty state — show example prompts when no conversation yet */}
|
||||
{messages.length === 0 && !isStreaming && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text dimColor>Try asking:</Text>
|
||||
<Box flexDirection="column" paddingLeft={2} marginTop={1} gap={0}>
|
||||
<Text dimColor>{" weather in tokyo"}</Text>
|
||||
<Text dimColor>{" top hacker news stories"}</Text>
|
||||
<Text dimColor>{" tell me about vercel/next.js"}</Text>
|
||||
<Text dimColor>{" bitcoin price"}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Message history — collapsed when interactive wizard is active */}
|
||||
{liveSpec && !isStreaming ? (
|
||||
<>
|
||||
@@ -568,10 +619,9 @@ export function App() {
|
||||
{/* Live spec preview while streaming */}
|
||||
{isStreaming && streamingSpec && streamingSpec.root && (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
<Text bold>AI:</Text>
|
||||
<JSONUIProvider initialState={streamingSpec.state ?? {}}>
|
||||
<DisableFocus />
|
||||
<Renderer spec={streamingSpec} />
|
||||
<Renderer spec={streamingSpec} loading />
|
||||
</JSONUIProvider>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -275,7 +275,8 @@ export const getGitHubRepo = tool({
|
||||
const languageBreakdown = Object.entries(languages)
|
||||
.map(([lang, bytes]) => ({
|
||||
language: lang,
|
||||
percentage: Math.round((bytes / totalBytes) * 100),
|
||||
percentage:
|
||||
totalBytes > 0 ? Math.round((bytes / totalBytes) * 100) : 0,
|
||||
}))
|
||||
.sort((a, b) => b.percentage - a.percentage)
|
||||
.slice(0, 6);
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# example-mcp
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/mcp@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-mcp",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# example-no-ai
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/shadcn@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-no-ai",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-react-email
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-email@0.15.0
|
||||
|
||||
## 0.1.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-email",
|
||||
"version": "0.1.5",
|
||||
"version": "0.1.6",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-react-native
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-native@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-native",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"private": true,
|
||||
"main": "expo-router/entry",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-react-pdf
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react-pdf@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-pdf",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
# example-react-three-fiber
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/react-three-fiber@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-react-three-fiber",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-remotion
|
||||
|
||||
## 0.1.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/remotion@0.15.0
|
||||
|
||||
## 0.1.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-remotion",
|
||||
"version": "0.1.9",
|
||||
"version": "0.1.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-solid
|
||||
|
||||
## 0.1.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/solid@0.15.0
|
||||
|
||||
## 0.1.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-solid",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# com.example.json-render-demo
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-demo",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.11",
|
||||
"description": "Test",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# com.example.json-render-fullpage-demo
|
||||
|
||||
## 0.0.11
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.0.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "com.example.json-render-fullpage-demo",
|
||||
"version": "0.0.10",
|
||||
"version": "0.0.11",
|
||||
"description": "Full-page Stripe App example (alpha)",
|
||||
"private": true,
|
||||
"license": "~~proprietary~~",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# svelte-chat
|
||||
|
||||
## 0.0.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
|
||||
## 0.0.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "svelte-chat",
|
||||
"private": true,
|
||||
"version": "0.0.6",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-svelte
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
|
||||
## 0.1.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-svelte",
|
||||
"version": "0.1.6",
|
||||
"version": "0.1.7",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# vite-renderers
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
- @json-render/solid@0.15.0
|
||||
- @json-render/svelte@0.15.0
|
||||
- @json-render/vue@0.15.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vite-renderers",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# example-vue
|
||||
|
||||
## 0.1.8
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/vue@0.15.0
|
||||
|
||||
## 0.1.7
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "example-vue",
|
||||
"version": "0.1.7",
|
||||
"version": "0.1.8",
|
||||
"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)",
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/codegen
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/codegen",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Utilities for generating code from json-render UI trees",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
# @json-render/core
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
|
||||
|
||||
### New:
|
||||
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
|
||||
|
||||
### Improved:
|
||||
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/core",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "JSON becomes real things. Define your catalog, register your components, let AI generate.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/image
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/image",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Image renderer for @json-render/core. JSON becomes SVG and PNG images via Satori.",
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
# @json-render/ink
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- bf3a7ec: Add Ink terminal renderer for interactive terminal UIs.
|
||||
|
||||
### New:
|
||||
- **`@json-render/ink`** -- Terminal UI renderer for json-render, built on Ink. Includes 20+ standard components (Box, Text, Heading, Card, Table, TextInput, Select, MultiSelect, Tabs, etc.), action/validation/focus contexts, two-way state binding, and streaming via `useUIStream`. Server-safe entry points at `@json-render/ink/schema`, `@json-render/ink/catalog`, and `@json-render/ink/server`.
|
||||
|
||||
### Improved:
|
||||
- **Examples** -- new `ink-chat` terminal chat demo, `game-engine` 3D example using react-three-fiber, website examples page with live demos and search.
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/ink",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Ink terminal renderer for @json-render/core. JSON becomes terminal UIs.",
|
||||
"keywords": [
|
||||
@@ -57,7 +57,8 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"check-types": "tsc --noEmit"
|
||||
"check-types": "tsc --noEmit",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*",
|
||||
|
||||
+77
-11
@@ -259,11 +259,12 @@ export const standardComponentDefinitions = {
|
||||
borderStyle: z
|
||||
.enum(["single", "double", "round", "bold", "classic"])
|
||||
.nullable(),
|
||||
backgroundColor: z.string().nullable(),
|
||||
headerColor: z.string().nullable(),
|
||||
}),
|
||||
slots: [],
|
||||
description:
|
||||
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values.",
|
||||
"Tabular data display with headers and rows. Each row is a record mapping column keys to string values. Set both borderStyle and backgroundColor together so borders share the same shading.",
|
||||
example: {
|
||||
columns: [
|
||||
{ header: "Name", key: "name", width: 20 },
|
||||
@@ -306,7 +307,7 @@ export const standardComponentDefinitions = {
|
||||
example: {
|
||||
title: "package.json",
|
||||
subtitle: "Modified 2 hours ago",
|
||||
leading: "📄",
|
||||
leading: "*",
|
||||
trailing: "2.1 KB",
|
||||
},
|
||||
},
|
||||
@@ -314,16 +315,13 @@ export const standardComponentDefinitions = {
|
||||
Card: {
|
||||
props: z.object({
|
||||
title: z.string().nullable(),
|
||||
borderStyle: z
|
||||
.enum(["single", "double", "round", "bold", "classic"])
|
||||
.nullable(),
|
||||
borderColor: z.string().nullable(),
|
||||
backgroundColor: z.string().nullable(),
|
||||
padding: z.number().nullable(),
|
||||
}),
|
||||
slots: ["default"],
|
||||
description:
|
||||
"Bordered container with optional title. Use for grouping related content with a visual boundary.",
|
||||
example: { title: "Details", borderStyle: "round", padding: 1 },
|
||||
"Shaded container with optional title. Renders as a filled background area for grouping related content. Default background is a subtle dark shade.",
|
||||
example: { title: "Details", padding: 1 },
|
||||
},
|
||||
|
||||
KeyValue: {
|
||||
@@ -363,6 +361,74 @@ export const standardComponentDefinitions = {
|
||||
example: { text: "Build completed successfully", status: "success" },
|
||||
},
|
||||
|
||||
Metric: {
|
||||
props: z.object({
|
||||
label: z.string(),
|
||||
value: z.string(),
|
||||
detail: z.string().nullable(),
|
||||
trend: z.enum(["up", "down", "neutral"]).nullable(),
|
||||
}),
|
||||
slots: [],
|
||||
description:
|
||||
"Key metric display with prominent value and optional trend indicator. Use for important numbers that deserve visual emphasis (price, temperature, stars, market cap).",
|
||||
example: {
|
||||
label: "Price",
|
||||
value: "$70,686",
|
||||
detail: "24h change",
|
||||
trend: "up",
|
||||
},
|
||||
},
|
||||
|
||||
Callout: {
|
||||
props: z.object({
|
||||
type: z.enum(["info", "tip", "warning", "important"]).nullable(),
|
||||
title: z.string().nullable(),
|
||||
content: z.string(),
|
||||
}),
|
||||
slots: [],
|
||||
description:
|
||||
"Highlighted callout block with colored left border. Use for key takeaways, tips, warnings, or important notes that should stand out from surrounding content.",
|
||||
example: {
|
||||
type: "tip",
|
||||
title: "Key Takeaway",
|
||||
content:
|
||||
"Revenue peaked in FY2022 driven by iPhone 13/14 upgrade cycles.",
|
||||
},
|
||||
},
|
||||
|
||||
Timeline: {
|
||||
props: z.object({
|
||||
items: z.array(
|
||||
z.object({
|
||||
title: z.string(),
|
||||
description: z.string().nullable(),
|
||||
date: z.string().nullable(),
|
||||
status: z.enum(["completed", "current", "upcoming"]).nullable(),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
slots: [],
|
||||
description:
|
||||
"Vertical timeline showing ordered events, steps, or milestones. Each item has a status-colored dot, title, optional date, and optional description.",
|
||||
example: {
|
||||
items: [
|
||||
{
|
||||
title: "Project Started",
|
||||
description: "Initial commit and setup",
|
||||
date: "Jan 2024",
|
||||
status: "completed",
|
||||
},
|
||||
{
|
||||
title: "Beta Release",
|
||||
description: "Public beta launched",
|
||||
date: "Mar 2024",
|
||||
status: "current",
|
||||
},
|
||||
{ title: "v1.0", date: "Q2 2024", status: "upcoming" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
// ==========================================================================
|
||||
// Interactive Components
|
||||
// ==========================================================================
|
||||
@@ -468,9 +534,9 @@ export const standardComponentDefinitions = {
|
||||
"Tab bar navigation. Navigate with left/right arrow keys. Use $bindState on value to bind the active tab to state. Place child content inside and use visible conditions on children to show content for the active tab.",
|
||||
example: {
|
||||
tabs: [
|
||||
{ label: "Overview", value: "overview", icon: "📊" },
|
||||
{ label: "Logs", value: "logs", icon: "📋" },
|
||||
{ label: "Settings", value: "settings", icon: "⚙" },
|
||||
{ label: "Overview", value: "overview" },
|
||||
{ label: "Logs", value: "logs" },
|
||||
{ label: "Settings", value: "settings" },
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -325,6 +325,7 @@ function TableComponent({ element }: ComponentRenderProps) {
|
||||
}>;
|
||||
rows?: Array<Record<string, string>>;
|
||||
borderStyle?: string;
|
||||
backgroundColor?: string;
|
||||
headerColor?: string;
|
||||
};
|
||||
|
||||
@@ -363,7 +364,12 @@ function TableComponent({ element }: ComponentRenderProps) {
|
||||
| undefined;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" borderStyle={borderStyleProp}>
|
||||
<Box
|
||||
flexDirection="column"
|
||||
alignSelf="flex-start"
|
||||
borderStyle={borderStyleProp}
|
||||
backgroundColor={p.backgroundColor}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box>
|
||||
{columns.map((col, i) => (
|
||||
@@ -375,15 +381,19 @@ function TableComponent({ element }: ComponentRenderProps) {
|
||||
{/* Separator */}
|
||||
<Text dimColor>{colWidths.map((w) => "─".repeat(w)).join("─")}</Text>
|
||||
{/* Rows */}
|
||||
{rows.map((row, rowIdx) => (
|
||||
<Box key={rowIdx}>
|
||||
{columns.map((col, i) => (
|
||||
<Text key={col.key}>
|
||||
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
))}
|
||||
{rows.map((row, rowIdx) => {
|
||||
const rowKey =
|
||||
columns.map((c) => row[c.key] ?? "").join("\0") || String(rowIdx);
|
||||
return (
|
||||
<Box key={rowKey} gap={0}>
|
||||
{columns.map((col, i) => (
|
||||
<Text key={col.key}>
|
||||
{padCell(row[col.key] || "—", colWidths[i]!, col.align)}
|
||||
</Text>
|
||||
))}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
@@ -402,7 +412,7 @@ function ListComponent({ element }: ComponentRenderProps) {
|
||||
return (
|
||||
<Box flexDirection="column" gap={p.spacing ?? 0}>
|
||||
{items.map((item, i) => (
|
||||
<Box key={i} gap={1}>
|
||||
<Box key={`${i}:${item}`} gap={1}>
|
||||
<Text dimColor>{p.ordered ? `${i + 1}.` : bullet}</Text>
|
||||
<Text>{item}</Text>
|
||||
</Box>
|
||||
@@ -434,16 +444,15 @@ function ListItemComponent({ element }: ComponentRenderProps) {
|
||||
function CardComponent({ element, children }: ComponentRenderProps) {
|
||||
const p = element.props as {
|
||||
title?: string;
|
||||
borderStyle?: "single" | "double" | "round" | "bold" | "classic";
|
||||
borderColor?: string;
|
||||
backgroundColor?: string;
|
||||
padding?: number;
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle={p.borderStyle ?? "round"}
|
||||
borderColor={p.borderColor}
|
||||
alignSelf="flex-start"
|
||||
backgroundColor={p.backgroundColor ?? "#1a1a1a"}
|
||||
padding={p.padding ?? 1}
|
||||
>
|
||||
{p.title ? (
|
||||
@@ -537,10 +546,136 @@ function StatusLineComponent({ element }: ComponentRenderProps) {
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Metric, Callout, Timeline
|
||||
// =============================================================================
|
||||
|
||||
const TREND_CONFIG: Record<string, { prefix: string; color: string }> = {
|
||||
up: { prefix: "+", color: "green" },
|
||||
down: { prefix: "", color: "red" },
|
||||
neutral: { prefix: "~", color: "gray" },
|
||||
};
|
||||
|
||||
function MetricComponent({ element }: ComponentRenderProps) {
|
||||
const p = element.props as {
|
||||
label?: string;
|
||||
value?: string;
|
||||
detail?: string;
|
||||
trend?: "up" | "down" | "neutral";
|
||||
};
|
||||
|
||||
const trend = p.trend ? TREND_CONFIG[p.trend] : null;
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text dimColor>{p.label ?? ""}</Text>
|
||||
<Box gap={1}>
|
||||
<Text bold>{p.value ?? ""}</Text>
|
||||
{trend && p.detail ? (
|
||||
<Text color={trend.color}>
|
||||
{trend.prefix}
|
||||
{p.detail}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
{!trend && p.detail ? <Text dimColor>{p.detail}</Text> : null}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const CALLOUT_CONFIG: Record<string, { label: string; color: string }> = {
|
||||
info: { label: "INFO", color: "blue" },
|
||||
tip: { label: "TIP", color: "green" },
|
||||
warning: { label: "WARNING", color: "yellow" },
|
||||
important: { label: "IMPORTANT", color: "magenta" },
|
||||
};
|
||||
|
||||
function CalloutComponent({ element }: ComponentRenderProps) {
|
||||
const p = element.props as {
|
||||
type?: "info" | "tip" | "warning" | "important";
|
||||
title?: string;
|
||||
content?: string;
|
||||
};
|
||||
|
||||
const config = CALLOUT_CONFIG[p.type ?? "info"] ?? CALLOUT_CONFIG.info!;
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="bold"
|
||||
borderLeft
|
||||
borderRight={false}
|
||||
borderTop={false}
|
||||
borderBottom={false}
|
||||
borderColor={config.color}
|
||||
paddingLeft={1}
|
||||
>
|
||||
{p.title ? (
|
||||
<Text bold color={config.color}>
|
||||
{p.title}
|
||||
</Text>
|
||||
) : null}
|
||||
<Text>{p.content ?? ""}</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
const TIMELINE_DOT: Record<string, { dot: string; color: string }> = {
|
||||
completed: { dot: "●", color: "green" },
|
||||
current: { dot: "◆", color: "cyan" },
|
||||
upcoming: { dot: "○", color: "gray" },
|
||||
};
|
||||
|
||||
function TimelineComponent({ element }: ComponentRenderProps) {
|
||||
const p = element.props as {
|
||||
items?: Array<{
|
||||
title?: string;
|
||||
description?: string;
|
||||
date?: string;
|
||||
status?: "completed" | "current" | "upcoming";
|
||||
}>;
|
||||
};
|
||||
|
||||
const items = p.items ?? [];
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" gap={1}>
|
||||
{items.map((item, i) => {
|
||||
const cfg =
|
||||
TIMELINE_DOT[item.status ?? "upcoming"] ?? TIMELINE_DOT.upcoming!;
|
||||
return (
|
||||
<Box key={i} flexDirection="column">
|
||||
<Box gap={1}>
|
||||
<Text color={cfg.color}>{cfg.dot}</Text>
|
||||
<Text bold>{item.title ?? ""}</Text>
|
||||
{item.date ? <Text dimColor>{item.date}</Text> : null}
|
||||
</Box>
|
||||
{item.description ? (
|
||||
<Box paddingLeft={2}>
|
||||
<Text dimColor>{item.description}</Text>
|
||||
</Box>
|
||||
) : null}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Interactive Components
|
||||
// =============================================================================
|
||||
|
||||
/** Count code points instead of UTF-16 code units (handles emoji/surrogate pairs). */
|
||||
function codePointLength(s: string): number {
|
||||
return Array.from(s).length;
|
||||
}
|
||||
|
||||
/** Slice by code point index instead of UTF-16 offset. */
|
||||
function codePointSlice(s: string, start: number, end?: number): string {
|
||||
return Array.from(s).slice(start, end).join("");
|
||||
}
|
||||
|
||||
function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
const p = element.props as {
|
||||
placeholder?: string;
|
||||
@@ -556,11 +691,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
);
|
||||
|
||||
const currentValue = value ?? "";
|
||||
const [cursorPos, setCursorPos] = useState(currentValue.length);
|
||||
const [cursorPos, setCursorPos] = useState(codePointLength(currentValue));
|
||||
|
||||
// Keep cursor in bounds when value changes externally
|
||||
useEffect(() => {
|
||||
setCursorPos((prev) => Math.min(prev, currentValue.length));
|
||||
setCursorPos((prev) => Math.min(prev, codePointLength(currentValue)));
|
||||
}, [currentValue]);
|
||||
|
||||
useInput(
|
||||
@@ -573,8 +708,8 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
if (key.backspace || key.delete) {
|
||||
if (cursorPos > 0) {
|
||||
const newVal =
|
||||
currentValue.slice(0, cursorPos - 1) +
|
||||
currentValue.slice(cursorPos);
|
||||
codePointSlice(currentValue, 0, cursorPos - 1) +
|
||||
codePointSlice(currentValue, cursorPos);
|
||||
setValue(newVal);
|
||||
setCursorPos((prev) => prev - 1);
|
||||
emit("change");
|
||||
@@ -588,7 +723,9 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
return;
|
||||
}
|
||||
if (key.rightArrow) {
|
||||
setCursorPos((prev) => Math.min(currentValue.length, prev + 1));
|
||||
setCursorPos((prev) =>
|
||||
Math.min(codePointLength(currentValue), prev + 1),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -598,11 +735,11 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
|
||||
if (input) {
|
||||
const newVal =
|
||||
currentValue.slice(0, cursorPos) +
|
||||
codePointSlice(currentValue, 0, cursorPos) +
|
||||
input +
|
||||
currentValue.slice(cursorPos);
|
||||
codePointSlice(currentValue, cursorPos);
|
||||
setValue(newVal);
|
||||
setCursorPos((prev) => prev + input.length);
|
||||
setCursorPos((prev) => prev + codePointLength(input));
|
||||
emit("change");
|
||||
}
|
||||
},
|
||||
@@ -611,13 +748,12 @@ function TextInputComponent({ element, emit, bindings }: ComponentRenderProps) {
|
||||
|
||||
const displayValue = currentValue
|
||||
? p.mask
|
||||
? p.mask.repeat(currentValue.length)
|
||||
? p.mask.repeat(codePointLength(currentValue))
|
||||
: currentValue
|
||||
: "";
|
||||
|
||||
// Render with cursor position indicator
|
||||
const before = displayValue.slice(0, cursorPos);
|
||||
const after = displayValue.slice(cursorPos);
|
||||
const before = codePointSlice(displayValue, 0, cursorPos);
|
||||
const after = codePointSlice(displayValue, cursorPos);
|
||||
|
||||
return (
|
||||
<Box gap={1}>
|
||||
@@ -1178,6 +1314,9 @@ export const standardComponents: ComponentRegistry = {
|
||||
KeyValue: KeyValueComponent,
|
||||
Link: LinkComponent,
|
||||
StatusLine: StatusLineComponent,
|
||||
Metric: MetricComponent,
|
||||
Callout: CalloutComponent,
|
||||
Timeline: TimelineComponent,
|
||||
TextInput: TextInputComponent,
|
||||
Select: SelectComponent,
|
||||
MultiSelect: MultiSelectComponent,
|
||||
|
||||
@@ -101,8 +101,8 @@ export interface PendingConfirmation {
|
||||
export interface ActionContextValue {
|
||||
/** Registered action handlers */
|
||||
handlers: Record<string, ActionHandler>;
|
||||
/** Actions currently executing (for loading indicators) */
|
||||
loadingActions: Set<string>;
|
||||
/** Actions currently executing (count of in-flight executions per action name) */
|
||||
loadingActions: Map<string, number>;
|
||||
/** Pending confirmation dialog */
|
||||
pendingConfirmation: PendingConfirmation | null;
|
||||
/** Execute an action binding */
|
||||
@@ -141,7 +141,9 @@ export function ActionProvider({
|
||||
initialHandlersRef.current = initialHandlers;
|
||||
const navigateRef = useRef(navigate);
|
||||
navigateRef.current = navigate;
|
||||
const [loadingActions, setLoadingActions] = useState<Set<string>>(new Set());
|
||||
const [loadingActions, setLoadingActions] = useState<Map<string, number>>(
|
||||
new Map(),
|
||||
);
|
||||
const [pendingConfirmation, setPendingConfirmation] =
|
||||
useState<PendingConfirmation | null>(null);
|
||||
// Ref tracks current pending confirmation so overlapping confirms can
|
||||
@@ -267,7 +269,11 @@ export function ActionProvider({
|
||||
}
|
||||
|
||||
const actionName = resolved.action;
|
||||
setLoadingActions((prev) => new Set(prev).add(actionName));
|
||||
setLoadingActions((prev) => {
|
||||
const next = new Map(prev);
|
||||
next.set(actionName, (next.get(actionName) ?? 0) + 1);
|
||||
return next;
|
||||
});
|
||||
try {
|
||||
await executeAction({
|
||||
action: resolved,
|
||||
@@ -281,8 +287,13 @@ export function ActionProvider({
|
||||
});
|
||||
} finally {
|
||||
setLoadingActions((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(actionName);
|
||||
const next = new Map(prev);
|
||||
const count = (next.get(actionName) ?? 1) - 1;
|
||||
if (count <= 0) {
|
||||
next.delete(actionName);
|
||||
} else {
|
||||
next.set(actionName, count);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}
|
||||
@@ -343,7 +354,7 @@ export function useAction(binding: ActionBinding): {
|
||||
} {
|
||||
const { execute, loadingActions } = useActions();
|
||||
const executeAction = useCallback(() => execute(binding), [execute, binding]);
|
||||
const isLoading = loadingActions.has(binding.action);
|
||||
const isLoading = (loadingActions.get(binding.action) ?? 0) > 0;
|
||||
return { execute: executeAction, isLoading };
|
||||
}
|
||||
|
||||
|
||||
@@ -99,7 +99,7 @@ function parsePatchLine(line: string): ParseResult {
|
||||
// Recovery: strip trailing extra braces/brackets one at a time
|
||||
// LLMs commonly generate extra closing characters in nested JSON
|
||||
let attempt = trimmed;
|
||||
for (let i = 0; i < 3; i++) {
|
||||
for (let i = 0; i < 8; i++) {
|
||||
const last = attempt[attempt.length - 1];
|
||||
if (last === "}" || last === "]") {
|
||||
attempt = attempt.slice(0, -1);
|
||||
|
||||
@@ -5,6 +5,7 @@ import React, {
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import { Text } from "ink";
|
||||
import type {
|
||||
UIElement,
|
||||
Spec,
|
||||
@@ -268,6 +269,9 @@ const ElementRenderer = React.memo(function ElementRenderer({
|
||||
console.warn(
|
||||
`[json-render] Missing element "${childKey}" referenced as child of "${resolvedElement.type}". This element will not render.`,
|
||||
);
|
||||
return (
|
||||
<Text key={childKey} color="red">{`[Missing: ${childKey}]`}</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -346,6 +350,12 @@ function RepeatChildren({
|
||||
console.warn(
|
||||
`[json-render] Missing element "${childKey}" referenced as child of "${element.type}" (repeat). This element will not render.`,
|
||||
);
|
||||
return (
|
||||
<Text
|
||||
key={`${key}:${childKey}`}
|
||||
color="red"
|
||||
>{`[Missing: ${childKey}]`}</Text>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/jotai
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/jotai",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Jotai adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/mcp
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/mcp",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.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,5 +1,12 @@
|
||||
# @json-render/react-email
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-email",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Email renderer for @json-render/core. JSON becomes HTML emails.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react-native
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-native",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Native renderer for @json-render/core. JSON becomes React Native components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react-pdf
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-pdf",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React PDF renderer for @json-render/core. JSON becomes PDF documents.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @internal/react-state
|
||||
|
||||
## 0.8.10
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.8.9
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@internal/react-state",
|
||||
"version": "0.8.9",
|
||||
"version": "0.8.10",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"description": "Shared React state context for json-render renderer packages",
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
# @json-render/react-three-fiber
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
- @json-render/react@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react-three-fiber",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React Three Fiber renderer for @json-render/core. JSON becomes 3D scenes.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/react
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/react",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "React renderer for @json-render/core. JSON becomes React components.",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/redux
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/redux",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Redux adapter for json-render StateStore",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
# @json-render/remotion
|
||||
|
||||
## 0.15.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [bf3a7ec]
|
||||
- @json-render/core@0.15.0
|
||||
|
||||
## 0.14.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@json-render/remotion",
|
||||
"version": "0.14.1",
|
||||
"version": "0.15.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "Remotion renderer for @json-render/core. JSON becomes video compositions.",
|
||||
"keywords": [
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# @json-render/shadcn-svelte
|
||||
|
||||
Pre-built [shadcn-svelte](https://www.shadcn-svelte.com/) components for json-render. Drop-in catalog definitions and Svelte implementations for 36 components built on Svelte 5 + Tailwind CSS.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
npm install @json-render/shadcn-svelte @json-render/core @json-render/svelte zod
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Create a Catalog
|
||||
|
||||
```ts
|
||||
import { schema } from "@json-render/svelte/schema";
|
||||
import { shadcnComponentDefinitions } from "@json-render/shadcn-svelte/catalog";
|
||||
|
||||
const catalog = schema.createCatalog({
|
||||
components: {
|
||||
Card: shadcnComponentDefinitions.Card,
|
||||
Stack: shadcnComponentDefinitions.Stack,
|
||||
Heading: shadcnComponentDefinitions.Heading,
|
||||
Button: shadcnComponentDefinitions.Button,
|
||||
Input: shadcnComponentDefinitions.Input,
|
||||
},
|
||||
actions: {},
|
||||
});
|
||||
```
|
||||
|
||||
### 2. Create a Registry
|
||||
|
||||
```ts
|
||||
import { defineRegistry } from "@json-render/svelte";
|
||||
import { shadcnComponents } from "@json-render/shadcn-svelte";
|
||||
|
||||
const { registry } = defineRegistry(catalog, {
|
||||
components: {
|
||||
Card: shadcnComponents.Card,
|
||||
Stack: shadcnComponents.Stack,
|
||||
Heading: shadcnComponents.Heading,
|
||||
Button: shadcnComponents.Button,
|
||||
Input: shadcnComponents.Input,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 3. Render
|
||||
|
||||
```svelte
|
||||
<script lang="ts">
|
||||
import { Renderer, JsonUIProvider } from "@json-render/svelte";
|
||||
|
||||
export let spec;
|
||||
export let registry;
|
||||
</script>
|
||||
|
||||
<JsonUIProvider initialState={spec?.state ?? {}}>
|
||||
<Renderer {spec} {registry} />
|
||||
</JsonUIProvider>
|
||||
```
|
||||
|
||||
## Exports
|
||||
|
||||
| Entry Point | Exports |
|
||||
|-------------|---------|
|
||||
| `@json-render/shadcn-svelte` | `shadcnComponents`, `shadcnComponentDefinitions` |
|
||||
| `@json-render/shadcn-svelte/catalog` | `shadcnComponentDefinitions` |
|
||||
|
||||
The `/catalog` entrypoint contains only Zod schemas (no renderer dependency), so it can be used in server-side code for prompt generation.
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "https://shadcn-svelte.com/schema.json",
|
||||
"tailwind": {
|
||||
"css": "src/app.css",
|
||||
"baseColor": "neutral"
|
||||
},
|
||||
"aliases": {
|
||||
"components": "$ui",
|
||||
"utils": "$lib/utils",
|
||||
"ui": "$ui",
|
||||
"hooks": "$lib/hooks",
|
||||
"lib": "$lib"
|
||||
},
|
||||
"typescript": true,
|
||||
"registry": "https://shadcn-svelte.com/registry",
|
||||
"style": "vega",
|
||||
"iconLibrary": "lucide",
|
||||
"menuColor": "default",
|
||||
"menuAccent": "subtle"
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
{
|
||||
"name": "@json-render/shadcn-svelte",
|
||||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"description": "shadcn-svelte component library for @json-render/svelte. JSON becomes beautiful Tailwind-styled Svelte components.",
|
||||
"keywords": [
|
||||
"json",
|
||||
"ui",
|
||||
"svelte",
|
||||
"svelte5",
|
||||
"shadcn-svelte",
|
||||
"tailwind",
|
||||
"bits-ui",
|
||||
"ai",
|
||||
"generative-ui",
|
||||
"llm",
|
||||
"renderer",
|
||||
"streaming",
|
||||
"components"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/vercel-labs/json-render.git",
|
||||
"directory": "packages/shadcn-svelte"
|
||||
},
|
||||
"homepage": "https://github.com/vercel-labs/json-render#readme",
|
||||
"bugs": {
|
||||
"url": "https://github.com/vercel-labs/json-render/issues"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "module",
|
||||
"svelte": "./dist/index.js",
|
||||
"main": "./dist/index.js",
|
||||
"module": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"svelte": "./dist/index.js",
|
||||
"default": "./dist/index.js"
|
||||
},
|
||||
"./catalog": {
|
||||
"types": "./dist/catalog.d.ts",
|
||||
"svelte": "./dist/catalog.js",
|
||||
"default": "./dist/catalog.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "svelte-package -i src -o dist",
|
||||
"dev": "svelte-package -i src -o dist --watch",
|
||||
"check-types": "svelte-check --tsconfig ./tsconfig.json",
|
||||
"typecheck": "svelte-check --tsconfig ./tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@json-render/core": "workspace:*",
|
||||
"@json-render/svelte": "workspace:*",
|
||||
"@lucide/svelte": "^0.577.0",
|
||||
"bits-ui": "^2.16.3",
|
||||
"clsx": "^2.1.1",
|
||||
"embla-carousel-svelte": "^8.6.0",
|
||||
"tailwind-merge": "^3.5.0",
|
||||
"tailwind-variants": "^3.2.2",
|
||||
"vaul-svelte": "1.0.0-next.7",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@fontsource-variable/inter": "^5.2.8",
|
||||
"@internal/typescript-config": "workspace:*",
|
||||
"@internationalized/date": "^3.12.0",
|
||||
"@sveltejs/package": "^2.5.7",
|
||||
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
||||
"svelte": "^5.54.1",
|
||||
"svelte-check": "^4.4.5",
|
||||
"tw-animate-css": "^1.4.0",
|
||||
"typescript": "^5.9.3"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.0.0",
|
||||
"tailwindcss": "^4.0.0",
|
||||
"zod": "^4.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import * as Accordion from "./ui/accordion/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Accordion">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const items = $derived(props.items ?? []);
|
||||
</script>
|
||||
|
||||
<Accordion.Root type={props.type ?? "single"} class="w-full">
|
||||
{#each items as item, i}
|
||||
<Accordion.Item value={`item-${i}`}>
|
||||
<Accordion.Trigger>{item.title}</Accordion.Trigger>
|
||||
<Accordion.Content>{item.content}</Accordion.Content>
|
||||
</Accordion.Item>
|
||||
{/each}
|
||||
</Accordion.Root>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { Alert, AlertDescription, AlertTitle } from "./ui/alert/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Alert">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const variant = $derived(props.type === "error" ? "destructive" : "default");
|
||||
</script>
|
||||
|
||||
<Alert {variant}>
|
||||
<AlertTitle>{props.title}</AlertTitle>
|
||||
{#if props.message}
|
||||
<AlertDescription>{props.message}</AlertDescription>
|
||||
{/if}
|
||||
</Alert>
|
||||
@@ -0,0 +1,29 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Avatar">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const initials = $derived(
|
||||
props.name
|
||||
.split(" ")
|
||||
.map((part) => part[0] ?? "")
|
||||
.slice(0, 2)
|
||||
.join("")
|
||||
.toUpperCase(),
|
||||
);
|
||||
|
||||
const sizeClass = $derived(
|
||||
props.size === "sm" ? "size-8 text-xs" : props.size === "lg" ? "size-12 text-base" : "size-10 text-sm",
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class={`inline-flex items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground ${sizeClass}`}>
|
||||
{#if props.src}
|
||||
<img src={props.src} alt={props.name} class="h-full w-full object-cover" />
|
||||
{:else}
|
||||
<span>{initials}</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,11 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { Badge } from "./ui/badge/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Badge">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Badge variant={props.variant ?? "default"}>{props.text}</Badge>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { Button } from "./ui/button/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Button">> {}
|
||||
|
||||
let { props, emit }: Props = $props();
|
||||
|
||||
const rawVariant = $derived((props.variant ?? "primary") as string);
|
||||
const variant = $derived(
|
||||
rawVariant === "danger" || rawVariant === "destructive"
|
||||
? "destructive"
|
||||
: rawVariant === "secondary" || rawVariant === "outline" || rawVariant === "ghost"
|
||||
? "secondary"
|
||||
: "default",
|
||||
);
|
||||
</script>
|
||||
|
||||
<Button {variant} disabled={props.disabled ?? false} onclick={() => emit("press")}>
|
||||
{props.label}
|
||||
</Button>
|
||||
@@ -0,0 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { getBoundProp } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"ButtonGroup">> {}
|
||||
|
||||
let { props, bindings, emit }: Props = $props();
|
||||
|
||||
let localValue = $state("");
|
||||
|
||||
const bound = getBoundProp<string>(
|
||||
() => props.selected ?? undefined,
|
||||
() => bindings?.selected,
|
||||
);
|
||||
|
||||
const value = $derived(bound.current ?? localValue);
|
||||
|
||||
function choose(next: string) {
|
||||
localValue = next;
|
||||
bound.current = next;
|
||||
emit("change");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="inline-flex rounded-md border border-border">
|
||||
{#each props.buttons ?? [] as btn, i}
|
||||
<button
|
||||
type="button"
|
||||
class={`px-3 py-1.5 text-sm ${value === btn.value ? "bg-primary text-primary-foreground" : "bg-background hover:bg-muted"} ${i > 0 ? "border-l border-border" : ""}`}
|
||||
onclick={() => choose(btn.value)}
|
||||
>
|
||||
{btn.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import * as Card from "./ui/card/index.js";
|
||||
import { cn } from "./lib/utils.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Card">> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
|
||||
const maxWidthClass = $derived(
|
||||
props.maxWidth === "sm"
|
||||
? "max-w-xs sm:min-w-[280px]"
|
||||
: props.maxWidth === "md"
|
||||
? "max-w-sm sm:min-w-[320px]"
|
||||
: props.maxWidth === "lg"
|
||||
? "max-w-md sm:min-w-[360px]"
|
||||
: "w-full",
|
||||
);
|
||||
</script>
|
||||
|
||||
<Card.Root class={cn(maxWidthClass, props.centered ? "mx-auto" : "") }>
|
||||
{#if props.title || props.description}
|
||||
<Card.Header>
|
||||
{#if props.title}
|
||||
<Card.Title>{props.title}</Card.Title>
|
||||
{/if}
|
||||
{#if props.description}
|
||||
<Card.Description>{props.description}</Card.Description>
|
||||
{/if}
|
||||
</Card.Header>
|
||||
{/if}
|
||||
<Card.Content class="flex flex-col gap-3">
|
||||
{@render children?.()}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
@@ -0,0 +1,38 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
Carousel,
|
||||
CarouselContent,
|
||||
CarouselItem,
|
||||
CarouselNext,
|
||||
CarouselPrevious,
|
||||
} from "./ui/carousel/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Carousel">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const items = $derived(props.items ?? []);
|
||||
</script>
|
||||
|
||||
<div class="relative px-10">
|
||||
<Carousel>
|
||||
<CarouselContent>
|
||||
{#each items as item}
|
||||
<CarouselItem class="basis-64">
|
||||
<div class="rounded-lg border bg-card p-4">
|
||||
{#if item.title}
|
||||
<h4 class="font-medium">{item.title}</h4>
|
||||
{/if}
|
||||
{#if item.description}
|
||||
<p class="text-sm text-muted-foreground">{item.description}</p>
|
||||
{/if}
|
||||
</div>
|
||||
</CarouselItem>
|
||||
{/each}
|
||||
</CarouselContent>
|
||||
<CarouselPrevious />
|
||||
<CarouselNext />
|
||||
</Carousel>
|
||||
</div>
|
||||
@@ -0,0 +1,60 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { createValidation } from "./helpers.js";
|
||||
import { Checkbox } from "./ui/checkbox/index.js";
|
||||
import { Label } from "./ui/label/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Checkbox">> {}
|
||||
|
||||
let { props, bindings, emit }: Props = $props();
|
||||
|
||||
let localChecked = $state(untrack(() => Boolean(props.checked)));
|
||||
let errors = $state<string[]>([]);
|
||||
|
||||
const validateOn = $derived((props.validateOn ?? "change") as "change" | "blur" | "submit");
|
||||
const validationCtx = getOptionalValidationContext();
|
||||
const validation = createValidation(
|
||||
validationCtx,
|
||||
untrack(() => bindings?.checked),
|
||||
untrack(() => props.checks ?? null),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const on = validateOn;
|
||||
untrack(() => {
|
||||
if (validation.hasValidation) validation.register(on);
|
||||
});
|
||||
});
|
||||
|
||||
const bound = getBoundProp<boolean>(
|
||||
() => props.checked ?? undefined,
|
||||
() => bindings?.checked,
|
||||
);
|
||||
|
||||
const checked = $derived(bound.current ?? localChecked);
|
||||
|
||||
function handleChange(value: boolean | "indeterminate") {
|
||||
const next = value === true;
|
||||
localChecked = next;
|
||||
bound.current = next;
|
||||
if (validateOn === "change") errors = validation.run(validateOn);
|
||||
emit("change");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id={props.name}
|
||||
{checked}
|
||||
onCheckedChange={handleChange}
|
||||
/>
|
||||
<Label for={props.name} class="text-sm cursor-pointer">{props.label}</Label>
|
||||
</div>
|
||||
{#if errors.length > 0}
|
||||
<p class="text-sm text-destructive">{errors[0]}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "./ui/collapsible/index.js";
|
||||
import { Button } from "./ui/button/index.js";
|
||||
import ChevronDown from "@lucide/svelte/icons/chevron-down";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Collapsible">> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
|
||||
let open = $state(untrack(() => props.defaultOpen ?? false));
|
||||
</script>
|
||||
|
||||
<Collapsible bind:open class="w-full rounded-md border border-border">
|
||||
<CollapsibleTrigger>
|
||||
{#snippet child({ props: triggerProps })}
|
||||
<Button
|
||||
variant="ghost"
|
||||
class="flex w-full items-center justify-between px-4 py-2 text-sm font-medium"
|
||||
{...triggerProps}
|
||||
>
|
||||
<span>{props.title}</span>
|
||||
<ChevronDown class={`size-4 transition-transform ${open ? "rotate-180" : ""}`} />
|
||||
</Button>
|
||||
{/snippet}
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div class="px-4 pb-3 pt-1">{@render children?.()}</div>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { getStateValue } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "./ui/dialog/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Dialog">> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
|
||||
const openState = getStateValue(untrack(() => props.openPath));
|
||||
const open = $derived(Boolean(openState.current));
|
||||
|
||||
function handleOpenChange(next: boolean) {
|
||||
openState.current = next;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog {open} onOpenChange={handleOpenChange}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{props.title}</DialogTitle>
|
||||
{#if props.description}
|
||||
<DialogDescription>{props.description}</DialogDescription>
|
||||
{/if}
|
||||
</DialogHeader>
|
||||
{@render children?.()}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { getStateValue } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerHeader,
|
||||
DrawerTitle,
|
||||
} from "./ui/drawer/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Drawer">> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
|
||||
const openState = getStateValue(untrack(() => props.openPath));
|
||||
const open = $derived(Boolean(openState.current));
|
||||
|
||||
function handleOpenChange(next: boolean) {
|
||||
openState.current = next;
|
||||
}
|
||||
</script>
|
||||
|
||||
<Drawer {open} onOpenChange={handleOpenChange}>
|
||||
<DrawerContent>
|
||||
<DrawerHeader>
|
||||
<DrawerTitle>{props.title}</DrawerTitle>
|
||||
{#if props.description}
|
||||
<DrawerDescription>{props.description}</DrawerDescription>
|
||||
{/if}
|
||||
</DrawerHeader>
|
||||
<div class="px-4 pb-4">
|
||||
{@render children?.()}
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -0,0 +1,41 @@
|
||||
<script lang="ts">
|
||||
import { getBoundProp } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "./ui/dropdown-menu/index.js";
|
||||
import { Button } from "./ui/button/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"DropdownMenu">> {}
|
||||
|
||||
let { props, bindings, emit }: Props = $props();
|
||||
|
||||
const valueBound = getBoundProp<string>(
|
||||
() => props.value ?? undefined,
|
||||
() => bindings?.value,
|
||||
);
|
||||
|
||||
function select(value: string) {
|
||||
valueBound.current = value;
|
||||
emit("select");
|
||||
}
|
||||
</script>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
{#snippet child({ props: triggerProps })}
|
||||
<Button variant="outline" {...triggerProps}>{props.label}</Button>
|
||||
{/snippet}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{#each props.items ?? [] as item}
|
||||
<DropdownMenuItem onclick={() => select(item.value)}>
|
||||
{item.label}
|
||||
</DropdownMenuItem>
|
||||
{/each}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Grid">> {
|
||||
children?: Snippet;
|
||||
}
|
||||
|
||||
let { props, children }: Props = $props();
|
||||
|
||||
const colsMap: Record<number, string> = {
|
||||
1: "grid-cols-1",
|
||||
2: "grid-cols-2",
|
||||
3: "grid-cols-3",
|
||||
4: "grid-cols-4",
|
||||
5: "grid-cols-5",
|
||||
6: "grid-cols-6",
|
||||
};
|
||||
|
||||
const gapMap: Record<string, string> = {
|
||||
sm: "gap-2",
|
||||
md: "gap-3",
|
||||
lg: "gap-4",
|
||||
};
|
||||
|
||||
const n = $derived(Math.max(1, Math.min(6, props.columns ?? 1)));
|
||||
</script>
|
||||
|
||||
<div class={`grid ${colsMap[n] ?? "grid-cols-1"} ${gapMap[props.gap ?? "md"] ?? "gap-3"}`}>
|
||||
{@render children?.()}
|
||||
</div>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Heading">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const level = $derived(props.level ?? "h2");
|
||||
const className = $derived(
|
||||
level === "h1"
|
||||
? "text-3xl font-bold tracking-tight"
|
||||
: level === "h2"
|
||||
? "text-2xl font-semibold tracking-tight"
|
||||
: level === "h3"
|
||||
? "text-xl font-semibold"
|
||||
: "text-lg font-medium",
|
||||
);
|
||||
</script>
|
||||
|
||||
<svelte:element this={level} class={className}>{props.text}</svelte:element>
|
||||
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Image">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if props.src}
|
||||
<img
|
||||
src={props.src}
|
||||
alt={props.alt}
|
||||
width={props.width ?? undefined}
|
||||
height={props.height ?? undefined}
|
||||
class="max-w-full rounded-md border border-border object-cover"
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="flex items-center justify-center rounded-md border border-dashed border-border bg-muted text-sm text-muted-foreground"
|
||||
style={`width:${props.width ?? 320}px;height:${props.height ?? 180}px`}
|
||||
>
|
||||
{props.alt}
|
||||
</div>
|
||||
{/if}
|
||||
@@ -0,0 +1,70 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { getBoundProp, getOptionalValidationContext } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { Input } from "./ui/input/index.js";
|
||||
import { Label } from "./ui/label/index.js";
|
||||
import { createValidation } from "./helpers.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Input">> {}
|
||||
|
||||
let { props, bindings, emit }: Props = $props();
|
||||
|
||||
let localValue = $state("");
|
||||
let errors = $state<string[]>([]);
|
||||
|
||||
const validateOn = $derived((props.validateOn ?? "blur") as "change" | "blur" | "submit");
|
||||
const validationCtx = getOptionalValidationContext();
|
||||
const validation = createValidation(
|
||||
validationCtx,
|
||||
untrack(() => bindings?.value),
|
||||
untrack(() => props.checks ?? null),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
const on = validateOn;
|
||||
untrack(() => {
|
||||
if (validation.hasValidation) validation.register(on);
|
||||
});
|
||||
});
|
||||
|
||||
const bound = getBoundProp<string>(
|
||||
() => props.value ?? undefined,
|
||||
() => bindings?.value,
|
||||
);
|
||||
|
||||
const value = $derived(bound.current ?? localValue);
|
||||
|
||||
function handleInput(event: Event) {
|
||||
const next = (event.target as HTMLInputElement).value;
|
||||
localValue = next;
|
||||
bound.current = next;
|
||||
if (validateOn === "change") errors = validation.run(validateOn);
|
||||
}
|
||||
|
||||
function handleBlur() {
|
||||
if (validateOn === "blur") errors = validation.run(validateOn);
|
||||
emit("blur");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for={props.name}>{props.label}</Label>
|
||||
<Input
|
||||
id={props.name}
|
||||
name={props.name}
|
||||
type={props.type ?? "text"}
|
||||
placeholder={props.placeholder ?? ""}
|
||||
{value}
|
||||
oninput={handleInput}
|
||||
onfocus={() => emit("focus")}
|
||||
onblur={handleBlur}
|
||||
onkeydown={(e: KeyboardEvent) => {
|
||||
if (e.key === "Enter") emit("submit");
|
||||
}}
|
||||
/>
|
||||
{#if errors.length > 0}
|
||||
<p class="text-sm text-destructive">{errors[0]}</p>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -0,0 +1,22 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Link">> {}
|
||||
|
||||
let { props, on }: Props = $props();
|
||||
|
||||
function handleClick(event: MouseEvent) {
|
||||
const press = on("press");
|
||||
if (press.shouldPreventDefault) event.preventDefault();
|
||||
press.emit();
|
||||
}
|
||||
</script>
|
||||
|
||||
<a
|
||||
href={props.href ?? "#"}
|
||||
class="text-primary text-sm font-medium underline-offset-4 hover:underline"
|
||||
onclick={handleClick}
|
||||
>
|
||||
{props.label}
|
||||
</a>
|
||||
@@ -0,0 +1,47 @@
|
||||
<script lang="ts">
|
||||
import { untrack } from "svelte";
|
||||
import { getBoundProp } from "@json-render/svelte";
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { getPaginationRange } from "./helpers.js";
|
||||
import { Button } from "./ui/button/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Pagination">> {}
|
||||
|
||||
let { props, bindings, emit }: Props = $props();
|
||||
|
||||
const pageBound = getBoundProp<number>(
|
||||
() => props.page ?? undefined,
|
||||
() => bindings?.page,
|
||||
);
|
||||
|
||||
let localPage = $state(untrack(() => props.page ?? 1));
|
||||
const page = $derived(pageBound.current ?? localPage);
|
||||
const totalPages = $derived(props.totalPages ?? 1);
|
||||
const pages = $derived(getPaginationRange(page, totalPages));
|
||||
|
||||
function setPage(next: number) {
|
||||
if (next < 1 || next > totalPages || next === page) return;
|
||||
localPage = next;
|
||||
pageBound.current = next;
|
||||
emit("change");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="inline-flex items-center gap-1">
|
||||
<Button variant="outline" size="sm" onclick={() => setPage(page - 1)} disabled={page <= 1}>Prev</Button>
|
||||
{#each pages as p}
|
||||
{#if p === "ellipsis"}
|
||||
<span class="px-2 text-sm text-muted-foreground">…</span>
|
||||
{:else}
|
||||
<Button
|
||||
variant={p === page ? "default" : "outline"}
|
||||
size="sm"
|
||||
onclick={() => setPage(p)}
|
||||
>
|
||||
{p}
|
||||
</Button>
|
||||
{/if}
|
||||
{/each}
|
||||
<Button variant="outline" size="sm" onclick={() => setPage(page + 1)} disabled={page >= totalPages}>Next</Button>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "./ui/popover/index.js";
|
||||
import { Button } from "./ui/button/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Popover">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
</script>
|
||||
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
{#snippet child({ props: triggerProps })}
|
||||
<Button variant="outline" {...triggerProps}>{props.trigger}</Button>
|
||||
{/snippet}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="w-64 text-sm">{props.content}</PopoverContent>
|
||||
</Popover>
|
||||
@@ -0,0 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { BaseComponentProps } from "@json-render/svelte";
|
||||
import type { ShadcnProps } from "./catalog.js";
|
||||
import { Progress } from "./ui/progress/index.js";
|
||||
|
||||
interface Props extends BaseComponentProps<ShadcnProps<"Progress">> {}
|
||||
|
||||
let { props }: Props = $props();
|
||||
|
||||
const max = $derived(props.max ?? 100);
|
||||
const percent = $derived(Math.max(0, Math.min(100, (props.value / max) * 100)));
|
||||
</script>
|
||||
|
||||
<div class="space-y-1">
|
||||
{#if props.label}
|
||||
<div class="text-sm text-muted-foreground">{props.label}</div>
|
||||
{/if}
|
||||
<Progress value={percent} />
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user