feat: dynamic mode
This commit is contained in:
@@ -1,23 +1,25 @@
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEditorStore } from "@/stores/editorStore";
|
||||
import { renderMicron } from "./micronRenderer";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type PreviewMode = "ascii" | "micron" | "raw";
|
||||
type PreviewMode = "ascii" | "micron" | "raw" | "script";
|
||||
|
||||
export default function PreviewPane() {
|
||||
const previewMode = useEditorStore((s) => s.previewMode);
|
||||
const setPreviewMode = useEditorStore((s) => s.setPreviewMode);
|
||||
const compiledAscii = useEditorStore((s) => s.compiledAscii);
|
||||
const compiledMicron = useEditorStore((s) => s.compiledMicron);
|
||||
const compiledScript = useEditorStore((s) => s.compiledScript);
|
||||
const isDynamic = useEditorStore((s) => s.isDynamic);
|
||||
const isCompiling = useEditorStore((s) => s.isCompiling);
|
||||
const compileError = useEditorStore((s) => s.compileError);
|
||||
|
||||
const tabs: { value: PreviewMode; label: string }[] = [
|
||||
{ value: "ascii", label: "ASCII" },
|
||||
{ value: "micron", label: "Micron" },
|
||||
{ value: "raw", label: "Raw" },
|
||||
const tabs: { value: PreviewMode; label: string; show: boolean }[] = [
|
||||
{ value: "ascii", label: "ASCII", show: true },
|
||||
{ value: "micron", label: "Micron", show: true },
|
||||
{ value: "raw", label: "Raw", show: true },
|
||||
{ value: "script", label: "Script", show: isDynamic },
|
||||
];
|
||||
|
||||
return (
|
||||
@@ -25,6 +27,11 @@ export default function PreviewPane() {
|
||||
<div className="flex items-center px-3 py-1.5 border-b shrink-0 gap-2">
|
||||
<span className="text-xs text-muted-foreground flex-1">
|
||||
Preview
|
||||
{isDynamic && (
|
||||
<span className="ml-1.5 text-amber-400" title="This page has dynamic features (source, if, for)">
|
||||
⚡ dynamic
|
||||
</span>
|
||||
)}
|
||||
{isCompiling && (
|
||||
<span className="ml-2 text-yellow-500 animate-pulse">
|
||||
compiling…
|
||||
@@ -37,20 +44,22 @@ export default function PreviewPane() {
|
||||
)}
|
||||
</span>
|
||||
<div className="flex gap-0.5 bg-muted rounded-md p-0.5">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setPreviewMode(tab.value)}
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded transition-colors",
|
||||
previewMode === tab.value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
{tabs
|
||||
.filter((t) => t.show)
|
||||
.map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setPreviewMode(tab.value)}
|
||||
className={cn(
|
||||
"text-xs px-2 py-0.5 rounded transition-colors",
|
||||
previewMode === tab.value
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 bg-background">
|
||||
@@ -77,6 +86,10 @@ export default function PreviewPane() {
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
) : previewMode === "script" ? (
|
||||
<pre className="p-4 font-mono text-xs whitespace-pre-wrap break-words text-amber-200/80 leading-relaxed">
|
||||
{compiledScript || "No dynamic script generated."}
|
||||
</pre>
|
||||
) : (
|
||||
<pre className="p-4 font-mono text-sm whitespace-pre-wrap break-words text-muted-foreground">
|
||||
{compiledMicron || "Raw Micron output will appear here…"}
|
||||
|
||||
@@ -227,4 +227,72 @@ export const EXAMPLES: Example[] = [
|
||||
col 30
|
||||
link "Settings" "/page/settings.mu"`,
|
||||
},
|
||||
{
|
||||
name: "Interactive Form",
|
||||
description: "Text fields, radio buttons, checkboxes, and submit",
|
||||
source: `page "Search" 56
|
||||
|
||||
box rounded "Node Search"
|
||||
align center
|
||||
text "Find peers and pages on the mesh"
|
||||
|
||||
spacer
|
||||
|
||||
form "search"
|
||||
field "query" 30 "Enter search term..."
|
||||
radio "scope" "Local" | "Network" | "All"
|
||||
checkbox "cache" "Include cached results"
|
||||
spacer
|
||||
button "Search" "/page/search.mu"
|
||||
|
||||
divider light
|
||||
|
||||
heading 2 "Quick Actions"
|
||||
|
||||
form "ping"
|
||||
field "target" 30 "Destination hash..."
|
||||
radio "mode" "Ping" | "Trace" | "Page"
|
||||
checkbox "verbose" "Verbose output"
|
||||
spacer
|
||||
button "Execute" "/page/action.mu"`,
|
||||
},
|
||||
{
|
||||
name: "Dynamic Dashboard",
|
||||
description: "Live data sources, conditionals, and cache control",
|
||||
source: `page "Live Status" 60
|
||||
cache 0
|
||||
|
||||
source cpu_pct : shell "grep 'cpu ' /proc/stat | awk '{print int(($2+$4)*100/($2+$4+$5))}'"
|
||||
source mem_pct : shell "free | awk '/Mem/{print int($3/$2*100)}'"
|
||||
source uptime : shell "uptime -p"
|
||||
source timestamp : python "datetime.now().strftime('%H:%M:%S')"
|
||||
|
||||
box double "Node Monitor"
|
||||
align center
|
||||
text "Live System Dashboard"
|
||||
text "Updated: $timestamp"
|
||||
|
||||
spacer
|
||||
|
||||
heading 1 "Resources"
|
||||
|
||||
gauge "CPU" $cpu_pct 100 28 warn=75 crit=90
|
||||
gauge "MEM" $mem_pct 100 28 warn=80 crit=95
|
||||
|
||||
spacer
|
||||
|
||||
if $cpu_pct > 90
|
||||
box heavy "ALERT"
|
||||
color f00
|
||||
text "CPU critical! Immediate action required."
|
||||
elif $cpu_pct > 75
|
||||
text "@color{ff0}{Warning: CPU usage elevated}"
|
||||
|
||||
spacer
|
||||
|
||||
label "Uptime" "$uptime"
|
||||
|
||||
divider heavy
|
||||
text "Press Ctrl+R to refresh"`,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -68,6 +68,31 @@ function renderInline(raw: string): string {
|
||||
} else if (code === "l") {
|
||||
out += `<span style="display:block;text-align:left">`;
|
||||
openTags.push("</span>"); i += 2; continue;
|
||||
} else if (code === "<") {
|
||||
// Form element: `<...> or `<!...> or `<?...> or `<^...>
|
||||
const closeAngle = raw.indexOf(">", i + 2);
|
||||
if (closeAngle !== -1) {
|
||||
const inner = raw.slice(i + 2, closeAngle);
|
||||
out += renderFormTag(inner);
|
||||
i = closeAngle + 1;
|
||||
continue;
|
||||
}
|
||||
} else if (code === "[") {
|
||||
// Link with inline formatting: `[`!Label`!`:/dest]
|
||||
const closeBracket = raw.indexOf("]", i + 2);
|
||||
if (closeBracket !== -1) {
|
||||
const inner = raw.slice(i + 2, closeBracket);
|
||||
const colonIdx = inner.indexOf("`:");
|
||||
if (colonIdx !== -1) {
|
||||
const labelRaw = inner.slice(0, colonIdx);
|
||||
const dest = inner.slice(colonIdx + 2);
|
||||
// Strip formatting tags from label for display
|
||||
const label = labelRaw.replace(/`[!*_]/g, "");
|
||||
out += `<a href="${escapeHtml(dest)}" style="color:#7dc4e4;text-decoration:underline">${escapeHtml(label)}</a>`;
|
||||
i = closeBracket + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +120,46 @@ function renderInline(raw: string): string {
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Render a Micron form tag `<...> as styled HTML. */
|
||||
function renderFormTag(inner: string): string {
|
||||
const esc = escapeHtml;
|
||||
|
||||
// Text field: width|name`placeholder or name`placeholder
|
||||
// Password: !width|name`placeholder
|
||||
// Checkbox: ?|name|value`label or ?|name|value|*`label
|
||||
// Radio: ^|group|value`label or ^|group|value|*`label
|
||||
|
||||
if (inner.startsWith("?")) {
|
||||
// Checkbox
|
||||
const backtick = inner.indexOf("`");
|
||||
const label = backtick !== -1 ? inner.slice(backtick + 1) : "";
|
||||
const checked = inner.includes("|*");
|
||||
const box = checked ? "☑" : "☐";
|
||||
return `<span style="color:#d4a8f8">${box} ${esc(label)}</span>`;
|
||||
}
|
||||
|
||||
if (inner.startsWith("^")) {
|
||||
// Radio button
|
||||
const backtick = inner.indexOf("`");
|
||||
const label = backtick !== -1 ? inner.slice(backtick + 1) : "";
|
||||
const selected = inner.includes("|*");
|
||||
const dot = selected ? "◉" : "○";
|
||||
return `<span style="color:#d4a8f8">${dot} ${esc(label)}</span>`;
|
||||
}
|
||||
|
||||
if (inner.startsWith("!")) {
|
||||
// Password field
|
||||
const backtick = inner.indexOf("`");
|
||||
const placeholder = backtick !== -1 ? inner.slice(backtick + 1) : "";
|
||||
return `<span style="color:#d4a8f8;border:1px solid rgba(212,168,248,0.3);border-radius:3px;padding:0 4px">🔒 ${esc(placeholder || "••••••")}</span>`;
|
||||
}
|
||||
|
||||
// Regular text field: width|name`placeholder or name`placeholder
|
||||
const backtick = inner.indexOf("`");
|
||||
const placeholder = backtick !== -1 ? inner.slice(backtick + 1) : "";
|
||||
return `<span style="color:#d4a8f8;border:1px solid rgba(212,168,248,0.3);border-radius:3px;padding:0 4px">${esc(placeholder || "...")}</span>`;
|
||||
}
|
||||
|
||||
/** Render a form element line as a styled badge. */
|
||||
function renderForm(line: string): string {
|
||||
const inner = escapeHtml(line);
|
||||
@@ -145,9 +210,6 @@ export function renderMicron(source: string): string {
|
||||
// Empty line
|
||||
} else if (line.trim() === "") {
|
||||
htmlLines.push("");
|
||||
// Form elements on their own line
|
||||
} else if (/^`?<[^>]+>$/.test(line)) {
|
||||
htmlLines.push(renderForm(line));
|
||||
} else {
|
||||
htmlLines.push(renderInline(line));
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function useCompile() {
|
||||
const compile = useCallback(
|
||||
async (source: string) => {
|
||||
if (!source.trim()) {
|
||||
setCompileResult("", "", []);
|
||||
setCompileResult("", "", "", false, []);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ export function useCompile() {
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
setCompileResult(data.ascii, data.micron, data.warnings || []);
|
||||
setCompileResult(data.ascii, data.micron, data.script || "", data.is_dynamic || false, data.warnings || []);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof DOMException && e.name === "AbortError") return;
|
||||
setCompileError(e instanceof Error ? e.message : "Compile failed");
|
||||
|
||||
@@ -18,21 +18,23 @@ interface EditorStore {
|
||||
// Compiled output
|
||||
compiledAscii: string;
|
||||
compiledMicron: string;
|
||||
compiledScript: string;
|
||||
isDynamic: boolean;
|
||||
compileWarnings: string[];
|
||||
isCompiling: boolean;
|
||||
compileError: string | null;
|
||||
|
||||
// Preview
|
||||
previewMode: "ascii" | "micron" | "raw";
|
||||
previewMode: "ascii" | "micron" | "raw" | "script";
|
||||
|
||||
// Actions
|
||||
setSource: (s: string) => void;
|
||||
setCurrentPage: (p: PageMeta | null) => void;
|
||||
setDirty: (v: boolean) => void;
|
||||
setCompileResult: (ascii: string, micron: string, warnings: string[]) => void;
|
||||
setCompileResult: (ascii: string, micron: string, script: string, isDynamic: boolean, warnings: string[]) => void;
|
||||
setCompiling: (v: boolean) => void;
|
||||
setCompileError: (e: string | null) => void;
|
||||
setPreviewMode: (mode: "ascii" | "micron" | "raw") => void;
|
||||
setPreviewMode: (mode: "ascii" | "micron" | "raw" | "script") => void;
|
||||
reset: () => void;
|
||||
}
|
||||
|
||||
@@ -43,6 +45,8 @@ export const useEditorStore = create<EditorStore>((set) => ({
|
||||
|
||||
compiledAscii: "",
|
||||
compiledMicron: "",
|
||||
compiledScript: "",
|
||||
isDynamic: false,
|
||||
compileWarnings: [],
|
||||
isCompiling: false,
|
||||
compileError: null,
|
||||
@@ -52,10 +56,12 @@ export const useEditorStore = create<EditorStore>((set) => ({
|
||||
setSource: (s) => set({ ufSource: s, isDirty: true }),
|
||||
setCurrentPage: (p) => set({ currentPage: p }),
|
||||
setDirty: (v) => set({ isDirty: v }),
|
||||
setCompileResult: (ascii, micron, warnings) =>
|
||||
setCompileResult: (ascii, micron, script, isDynamic, warnings) =>
|
||||
set({
|
||||
compiledAscii: ascii,
|
||||
compiledMicron: micron,
|
||||
compiledScript: script,
|
||||
isDynamic,
|
||||
compileWarnings: warnings,
|
||||
isCompiling: false,
|
||||
compileError: null,
|
||||
@@ -70,6 +76,8 @@ export const useEditorStore = create<EditorStore>((set) => ({
|
||||
currentPage: null,
|
||||
compiledAscii: "",
|
||||
compiledMicron: "",
|
||||
compiledScript: "",
|
||||
isDynamic: false,
|
||||
compileWarnings: [],
|
||||
isCompiling: false,
|
||||
compileError: null,
|
||||
|
||||
Reference in New Issue
Block a user