feat: preview

This commit is contained in:
2026-04-01 10:13:14 +02:00
parent 8776459ffb
commit 0316e50233
13 changed files with 590 additions and 538 deletions

View File

@@ -1,4 +1,3 @@
import { ScrollArea } from "@/components/ui/scroll-area";
import { useEditorStore } from "@/stores/editorStore";
import { renderMicron } from "./micronRenderer";
import { cn } from "@/lib/utils";
@@ -62,9 +61,9 @@ export default function PreviewPane() {
))}
</div>
</div>
<ScrollArea className="flex-1 bg-background">
<div className="flex-1 bg-background overflow-auto">
{previewMode === "ascii" ? (
<pre className="p-4 font-mono text-sm whitespace-pre leading-tight text-green-100/90">
<pre className="p-2 font-mono text-[11px] whitespace-pre leading-tight text-green-100/90">
{compiledAscii || (
<span className="text-muted-foreground">
ASCII preview will appear here
@@ -74,7 +73,7 @@ export default function PreviewPane() {
) : previewMode === "micron" ? (
compiledMicron ? (
<div
className="p-4 font-mono text-sm whitespace-pre leading-tight"
className="p-2 font-mono text-[11px] whitespace-pre leading-tight"
dangerouslySetInnerHTML={{
__html: renderMicron(compiledMicron),
}}
@@ -91,11 +90,11 @@ export default function PreviewPane() {
{compiledScript || "No dynamic script generated."}
</pre>
) : (
<pre className="p-4 font-mono text-sm whitespace-pre-wrap break-words text-muted-foreground">
<pre className="p-4 font-mono text-xs whitespace-pre-wrap break-words text-muted-foreground">
{compiledMicron || "Raw Micron output will appear here…"}
</pre>
)}
</ScrollArea>
</div>
</div>
);
}

View File

@@ -15,59 +15,82 @@ function escapeHtml(text: string): string {
function renderInline(raw: string): string {
let out = "";
let i = 0;
const openTags: string[] = [];
const closeAll = () => {
while (openTags.length) out += openTags.pop()!;
};
// Track open/close state for toggle-style tags
let boldOpen = false;
let italicOpen = false;
let underOpen = false;
let fgOpen = false;
let bgOpen = false;
let alignOpen = false;
while (i < raw.length) {
// Backtick formatting codes
if (raw[i] === "`") {
const code = raw[i + 1];
if (code === "!") {
out += "<strong>"; openTags.push("</strong>"); i += 2; continue;
if (boldOpen) { out += "</strong>"; boldOpen = false; }
else { out += "<strong>"; boldOpen = true; }
i += 2; continue;
} else if (code === "*") {
out += "<em>"; openTags.push("</em>"); i += 2; continue;
if (italicOpen) { out += "</em>"; italicOpen = false; }
else { out += "<em>"; italicOpen = true; }
i += 2; continue;
} else if (code === "_") {
out += "<u>"; openTags.push("</u>"); i += 2; continue;
if (underOpen) { out += "</u>"; underOpen = false; }
else { out += "<u>"; underOpen = true; }
i += 2; continue;
} else if (code === "`") {
closeAll(); i += 2; continue;
} else if (code === "f" || code === "b") {
out += "</span>"; i += 2; continue;
// Reset all
if (boldOpen) { out += "</strong>"; boldOpen = false; }
if (italicOpen) { out += "</em>"; italicOpen = false; }
if (underOpen) { out += "</u>"; underOpen = false; }
if (fgOpen) { out += "</span>"; fgOpen = false; }
if (bgOpen) { out += "</span>"; bgOpen = false; }
if (alignOpen) { out += "</span>"; alignOpen = false; }
i += 2; continue;
} else if (code === "f") {
if (fgOpen) { out += "</span>"; fgOpen = false; }
i += 2; continue;
} else if (code === "b") {
if (bgOpen) { out += "</span>"; bgOpen = false; }
i += 2; continue;
} else if (code === "a") {
out += "</span>"; i += 2; continue;
if (alignOpen) { out += "</span>"; alignOpen = false; }
i += 2; continue;
} else if (code === "F") {
// Foreground color — 3-digit hex only per spec
const hexMatch = raw.slice(i + 2).match(/^([0-9a-fA-F]{3})(?![0-9a-fA-F])/);
const hexMatch = raw.slice(i + 2).match(/^([0-9a-fA-F]{3})/);
if (hexMatch) {
if (fgOpen) { out += "</span>"; }
const [r, g, b] = hexMatch[1].split("");
const hex = r + r + g + g + b + b;
out += `<span style="color:#${hex}">`;
openTags.push("</span>");
fgOpen = true;
i += 2 + hexMatch[1].length;
continue;
}
} else if (code === "B") {
// Background color — 3-digit hex only per spec
const hexMatch = raw.slice(i + 2).match(/^([0-9a-fA-F]{3})(?![0-9a-fA-F])/);
const hexMatch = raw.slice(i + 2).match(/^([0-9a-fA-F]{3})/);
if (hexMatch) {
if (bgOpen) { out += "</span>"; }
const [r, g, b] = hexMatch[1].split("");
const hex = r + r + g + g + b + b;
out += `<span style="background:#${hex}">`;
openTags.push("</span>");
bgOpen = true;
i += 2 + hexMatch[1].length;
continue;
}
} else if (code === "c") {
out += `<span style="display:block;text-align:center">`;
openTags.push("</span>"); i += 2; continue;
alignOpen = true; i += 2; continue;
} else if (code === "r") {
out += `<span style="display:block;text-align:right">`;
openTags.push("</span>"); i += 2; continue;
alignOpen = true; i += 2; continue;
} else if (code === "l") {
out += `<span style="display:block;text-align:left">`;
openTags.push("</span>"); i += 2; continue;
alignOpen = true; i += 2; continue;
} else if (code === "<") {
// Form element: `<...> or `<!...> or `<?...> or `<^...>
const closeAngle = raw.indexOf(">", i + 2);
@@ -116,7 +139,13 @@ function renderInline(raw: string): string {
i++;
}
closeAll();
// Close any remaining open tags
if (boldOpen) out += "</strong>";
if (italicOpen) out += "</em>";
if (underOpen) out += "</u>";
if (fgOpen) out += "</span>";
if (bgOpen) out += "</span>";
if (alignOpen) out += "</span>";
return out;
}

View File

@@ -127,4 +127,34 @@
html {
@apply font-sans;
}
/* Dark-mode scrollbars */
* {
scrollbar-width: thin;
scrollbar-color: oklch(0.35 0 0) transparent;
}
*::-webkit-scrollbar {
width: 6px;
height: 6px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: oklch(0.35 0 0);
border-radius: 3px;
}
*::-webkit-scrollbar-thumb:hover {
background: oklch(0.45 0 0);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
.dark *::-webkit-scrollbar-thumb {
background: oklch(0.3 0 0);
}
.dark *::-webkit-scrollbar-thumb:hover {
background: oklch(0.4 0 0);
}
}