feat: templates

This commit is contained in:
2026-04-01 07:50:43 +02:00
parent b40c6436cd
commit df11705875
10 changed files with 589 additions and 19 deletions

View File

@@ -1,14 +1,25 @@
import { useState } from "react";
import { BookOpen } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverHeader,
PopoverTitle,
} from "@/components/ui/popover";
import { useEditorStore } from "@/stores/editorStore";
import { useBacklinks } from "@/hooks/useBacklinks";
import BacklinkIndicator from "@/components/editor/BacklinkIndicator";
import { EXAMPLES } from "@/components/editor/examples";
interface Props {
pageName: string;
onNameChange?: (name: string) => void;
onSaveDraft: () => void;
onPublish: () => void;
onInsertExample: (source: string) => void;
saving: boolean;
isDirty: boolean;
}
@@ -18,11 +29,13 @@ export default function ToolBar({
onNameChange,
onSaveDraft,
onPublish,
onInsertExample,
saving,
isDirty,
}: Props) {
const currentPage = useEditorStore((s) => s.currentPage);
const backlinks = useBacklinks(currentPage?.name);
const [examplesOpen, setExamplesOpen] = useState(false);
return (
<div className="flex items-center gap-3 px-4 py-2 border-b bg-card shrink-0">
@@ -37,6 +50,42 @@ export default function ToolBar({
<span className="font-mono font-semibold text-sm">{pageName}</span>
)}
<Popover open={examplesOpen} onOpenChange={setExamplesOpen}>
<PopoverTrigger
render={
<button
className="inline-flex items-center justify-center gap-1.5 rounded-md text-xs font-medium h-8 px-3 border border-input bg-background hover:bg-accent hover:text-accent-foreground transition-colors"
title="Insert example template"
>
<BookOpen className="h-3.5 w-3.5" />
<span>Examples</span>
</button>
}
/>
<PopoverContent side="bottom" align="start" sideOffset={8}>
<PopoverHeader>
<PopoverTitle>Insert Example</PopoverTitle>
</PopoverHeader>
<div className="flex flex-col gap-0.5 max-h-72 overflow-y-auto -mx-1">
{EXAMPLES.map((ex) => (
<button
key={ex.name}
onClick={() => {
onInsertExample(ex.source);
setExamplesOpen(false);
}}
className="flex flex-col items-start rounded-md px-2 py-1.5 text-left hover:bg-accent transition-colors"
>
<span className="text-sm font-medium">{ex.name}</span>
<span className="text-xs text-muted-foreground leading-tight">
{ex.description}
</span>
</button>
))}
</div>
</PopoverContent>
</Popover>
<div className="flex-1" />
<BacklinkIndicator backlinks={backlinks} />
@@ -45,7 +94,12 @@ export default function ToolBar({
<span className="text-xs text-muted-foreground">Unsaved</span>
)}
<Button variant="outline" size="sm" onClick={onSaveDraft} disabled={saving}>
<Button
variant="outline"
size="sm"
onClick={onSaveDraft}
disabled={saving}
>
Save Draft
</Button>
<Button size="sm" onClick={onPublish} disabled={saving}>

View File

@@ -0,0 +1,230 @@
export interface Example {
name: string;
description: string;
source: string;
}
export const EXAMPLES: Example[] = [
{
name: "Hello World",
description: "Minimal page with a heading and text",
source: `page "Hello" 50
heading 1 "Welcome"
text "This is your first page."
spacer
heading 2 "About"
text "Built with the uFrame DSL."
divider light
link "Home" "/page/index.mu"`,
},
{
name: "Node Dashboard",
description: "System gauges, status indicators, and network info",
source: `page "Node Status" 60
box double "Relay Alpha-7"
align center
text "Reticulum Network Node"
text "Online 14d 3h 22m"
spacer
heading 1 "Resources"
row 2
col 28
gauge "CPU" 62 100 24 warn=75 crit=90
col 28
gauge "MEM" 84 100 24 warn=80 crit=95
spacer
heading 1 "Network"
row 2
col 30
label "Peers" "7 / 12"
label "Uptime" "14d 3h 22m"
col 28
status "East Relay" online
status "South Bridge" online
status "Node Gamma" degraded
status "Hub North" offline
divider heavy
text "Press Ctrl+R to refresh"`,
},
{
name: "Routing Table",
description: "Box-drawn table with colored status indicators",
source: `page "Routes" 60
heading 1 "Routing Table"
table "Active Routes"
columns "Destination" 22 | "Hops" 6 | "RTT" 8 | "State" 12
row "a7f2::relay-east" | "2" | "34ms" | "@color{0f0}{● alive}"
row "c4e1::bridge-south" | "4" | "112ms" | "@color{0f0}{● alive}"
row "01ab::node-gamma" | "7" | "580ms" | "@color{f00}{○ stale}"
row "f390::hub-north" | "1" | "8ms" | "@color{0f0}{● alive}"
spacer
label "Total routes" "4"
label "Average RTT" "183ms"
divider light
link "Refresh" "/page/routes.mu"`,
},
{
name: "Nested Boxes",
description: "Four box styles with nested content",
source: `page "Box Styles" 50
heading 1 "Box Styles"
spacer
box light "Light Border"
text "Standard border style"
text "Good for general content"
spacer
box heavy "Heavy Border"
text "Thick borders for emphasis"
text "Use for alerts or highlights"
spacer
box double "Double Border"
text "Double-line borders"
text "Great for titles and headers"
spacer
box rounded "Rounded Border"
text "Soft corners"
text "A more modern feel"
spacer
heading 2 "Nested"
box double "Outer"
text "This box contains another:"
box light "Inner"
text "Nested content here"`,
},
{
name: "Data Visualization",
description: "Gauges, sparklines, and status indicators",
source: `page "Metrics" 60
box heavy "System Metrics"
align center
text "Real-time monitoring dashboard"
spacer
heading 1 "CPU & Memory"
gauge "CPU" 42 100 28
gauge "GPU" 21 100 28
gauge "MEM" 67 100 28 warn=80 crit=95
gauge "SWP" 3 100 28
spacer
heading 1 "Network Traffic"
sparkline "Inbound" "1,3,5,8,7,5,3,2,1,3,6,8,7,4" 20
sparkline "Outbound" "2,2,3,5,8,7,5,3,2,1,1,3,5,8" 20
spacer
heading 1 "Services"
row 2
col 28
status "NomadNet" online
status "LXMF Router" online
status "Sideband" online
col 28
status "Reticulum" online
status "TCP Interface" degraded
status "I2P Transport" offline
divider heavy
text "Last updated: just now"`,
},
{
name: "Full Node Page",
description: "Complete node page with all primitives",
source: `page "Relay Alpha-7" 64
box double "Relay Alpha-7"
align center
text "Reticulum Mesh Node"
text "Sector 7 - Grid Reference 4E"
spacer
heading 1 "Resources"
row 2
col 30
gauge "CPU" 62 100 26 warn=75 crit=90
gauge "MEM" 84 100 26 warn=80 crit=95
col 30
gauge "DISK" 45 100 26 warn=85 crit=95
gauge "NET" 23 100 26
spacer
heading 1 "Network"
sparkline "Traffic IN" "1,4,6,8,7,5,3,2,3,5,7,8,6,4" 20
sparkline "Traffic OUT" "2,3,5,7,8,6,4,3,4,6,8,7,5,3" 20
spacer
heading 1 "Routing"
table "Active Routes"
columns "Destination" 20 | "Hops" 6 | "RTT" 8 | "State" 10
row "relay-east" | "2" | "34ms" | "@color{0f0}{● up}"
row "bridge-south" | "4" | "112ms" | "@color{0f0}{● up}"
row "node-gamma" | "7" | "580ms" | "@color{f00}{○ down}"
row "hub-north" | "1" | "8ms" | "@color{0f0}{● up}"
spacer
heading 1 "Peers"
row 2
col 30
status "East Relay" online
status "South Bridge" online
status "Backup Node" degraded
col 30
label "Active" "7 / 12"
label "Uptime" "14d 3h"
label "Version" "0.7.2"
divider heavy
row 2
col 30
link "Home" "/page/index.mu"
col 30
link "Settings" "/page/settings.mu"`,
},
];

View File

@@ -145,6 +145,16 @@ const COMMANDS: CmdEntry[] = [
apply: insert("bold"),
},
// Table
{
label: "table",
detail: 'table + columns + rows',
section: "Data",
apply: insert(
`table "Title"\n columns "Name" 20 | "Value" 10\n row "entry" | "data"`,
),
},
// Templates
{
label: "dashboard",

View File

@@ -129,6 +129,7 @@ export default function EditorView() {
onNameChange={isNew ? setPageName : undefined}
onSaveDraft={() => handleSave(false)}
onPublish={() => handleSave(true)}
onInsertExample={(source) => setSource(source)}
saving={saving}
isDirty={isDirty}
/>