feat: styles

This commit is contained in:
2026-04-02 12:42:21 +02:00
parent 462d6bf289
commit fcf3dc74a8
14 changed files with 336 additions and 275 deletions

View File

@@ -30,20 +30,20 @@ from uframe.themes import BUILTIN_THEMES
# ---------------------------------------------------------------------------
register_keyword("page", node_class=Page, section="Layout",
detail='page "Title" 64', snippet='page "${title}" ${width:64}',
detail='page "Title" 64', snippet='page "${1:Title}" ${2:64}',
is_container=True)
register_keyword("box", node_class=Box, section="Layout",
detail='box light "Title"', snippet='box ${weight:light} "${title}"',
detail='box light "Title"', snippet='box ${1:light} "${2:Title}"',
highlight_values=["light", "heavy", "double", "rounded"],
is_container=True)
register_keyword("row", node_class=Row, section="Layout",
detail="row [gap]", snippet="row ${gap:2}",
detail="row [gap]", snippet="row ${1:2}",
is_container=True)
register_keyword("col", node_class=Col, section="Layout",
detail="col [width]", snippet="col ${width}",
detail="col [width]", snippet="col ${1}",
is_container=True)
register_keyword("spacer", node_class=Spacer, section="Layout",
@@ -51,7 +51,7 @@ register_keyword("spacer", node_class=Spacer, section="Layout",
is_leaf=True)
register_keyword("pad", node_class=Pad, section="Layout",
detail="pad t r b l", snippet="pad ${top:1} ${right:1} ${bottom:1} ${left:1}",
detail="pad t r b l", snippet="pad ${1:1} ${2:1} ${3:1} ${4:1}",
is_container=True)
# ---------------------------------------------------------------------------
@@ -59,28 +59,28 @@ register_keyword("pad", node_class=Pad, section="Layout",
# ---------------------------------------------------------------------------
register_keyword("heading", node_class=Heading, section="Content",
detail='heading 1 "Text"', snippet='heading ${level:1} "${text}"',
detail='heading 1 "Text"', snippet='heading ${1:1} "${2:Text}"',
is_leaf=True)
register_keyword("text", node_class=Text, section="Content",
detail='text "Content"', snippet='text "${content}"',
detail='text "Content"', snippet='text "${1:Content}"',
is_leaf=True)
register_keyword("label", node_class=Label, section="Content",
detail='label "Key" "Value"', snippet='label "${key}" "${value}"',
detail='label "Key" "Value"', snippet='label "${1:Key}" "${2:Value}"',
is_leaf=True)
register_keyword("divider", node_class=Divider, section="Content",
detail="divider heavy", snippet="divider ${style:light}",
detail="divider heavy", snippet="divider ${1:light}",
highlight_values=["light", "heavy", "double", "dash", "dot"],
is_leaf=True)
register_keyword("link", node_class=Link, section="Content",
detail='link "Text" "/path.mu"', snippet='link "${display}" "${dest}"',
detail='link "Text" "/path.mu"', snippet='link "${1:Text}" "${2:/page/dest.mu}"',
is_leaf=True)
register_keyword("list", node_class=ListNode, section="Content",
detail="list bullet", snippet='list ${style:bullet}\n item "${entry}"',
detail="list bullet", snippet='list ${1:bullet}\n item "${2:Entry}"',
highlight_values=["bullet", "dash", "number", "arrow"],
is_container=True)
@@ -94,17 +94,17 @@ register_keyword("item", node_class=ListItem, section="",
register_keyword("gauge", node_class=Gauge, section="Data",
detail="gauge label val max width",
snippet='gauge "${label}" ${value} ${max:100} ${width:28} warn=${warn:75} crit=${crit:90}',
snippet='gauge "${1:Label}" ${2:0} ${3:100} ${4:28} warn=${5:75} crit=${6:90}',
is_leaf=True)
register_keyword("sparkline", node_class=Sparkline, section="Data",
detail="sparkline label values width",
snippet='sparkline "${label}" "${values}" ${width:20}',
snippet='sparkline "${1:Label}" "${2:1,2,3,4}" ${3:20}',
is_leaf=True)
register_keyword("status", node_class=Status, section="Data",
detail="status label state",
snippet='status "${label}" ${state:online}',
snippet='status "${1:Label}" ${2:online}',
highlight_values=["online", "offline", "degraded", "unknown", "alert"],
is_leaf=True)
@@ -121,18 +121,18 @@ register_keyword("columns", section="",
# ---------------------------------------------------------------------------
register_keyword("hnav", node_class=HNav, section="Layout",
detail='hnav bar', snippet='hnav ${style:bar}\n item "${label}" "${dest}" active',
detail='hnav bar', snippet='hnav ${1:bar}\n item "${2:Label}" "${3:/page/dest.mu}" active',
highlight_values=["bar", "tabs", "pills", "breadcrumb", "underline"],
is_container=True)
register_keyword("vnav", node_class=VNav, section="Layout",
detail='vnav list', snippet='vnav ${style:list}\n item "${label}" "${dest}"',
detail='vnav list', snippet='vnav ${1:list}\n item "${2:Label}" "${3:/page/dest.mu}"',
highlight_values=["list", "boxed", "tree", "sidebar", "minimal"],
is_container=True)
register_keyword("image", node_class=ImageNode, section="Content",
detail='image "path.png" braille 30',
snippet='image "${path}" ${mode:braille} ${width:30}',
snippet='image "${1:path.png}" ${2:braille} ${3:30}',
highlight_values=["braille", "block", "ascii", "halfblock"],
is_leaf=True)
@@ -151,7 +151,7 @@ register_keyword("caption", section="",
register_keyword("bigtitle", node_class=BigTitle, section="Content",
detail='bigtitle "TEXT" block',
snippet='bigtitle "${text}" ${font:block}',
snippet='bigtitle "${1:TEXT}" ${2:block}',
highlight_values=["block", "thin", "pixel"],
is_leaf=True)
@@ -160,12 +160,12 @@ register_keyword("bigtitle", node_class=BigTitle, section="Content",
# ---------------------------------------------------------------------------
register_keyword("align", section="Style",
detail="align center", snippet="align ${align:center}",
detail="align center", snippet="align ${1:center}",
highlight_values=["left", "center", "right"],
is_style_directive=True)
register_keyword("color", section="Style",
detail="color hex", snippet="color ${hex}",
detail="color hex", snippet="color ${1:0cf}",
is_style_directive=True)
register_keyword("bg", section="Style",
@@ -189,32 +189,32 @@ register_keyword("underline", section="Style",
# ---------------------------------------------------------------------------
register_keyword("form", node_class=Form, section="Form",
detail='form "name"', snippet='form "${name}"',
detail='form "name"', snippet='form "${1:name}"',
is_container=True)
register_keyword("field", node_class=Field, section="Form",
detail='field "name" 24 "placeholder"',
snippet='field "${name}" ${width:24} "${placeholder}"',
snippet='field "${1:name}" ${2:24} "${3:placeholder}"',
is_leaf=True)
register_keyword("password", node_class=Password, section="Form",
detail='password "name" 24 "placeholder"',
snippet='password "${name}" ${width:24} "${placeholder}"',
snippet='password "${1:name}" ${2:24} "${3:placeholder}"',
is_leaf=True)
register_keyword("radio", node_class=Radio, section="Form",
detail='radio "group" "A" | "B" | "C"',
snippet='radio "${group}" "${opt1}" | "${opt2}"',
snippet='radio "${1:group}" "${2:Option A}" | "${3:Option B}"',
is_leaf=True)
register_keyword("checkbox", node_class=Checkbox, section="Form",
detail='checkbox "name" "Label"',
snippet='checkbox "${name}" "${label}"',
snippet='checkbox "${1:name}" "${2:Label}"',
is_leaf=True)
register_keyword("button", node_class=FormButton, section="Form",
detail='button "Label" "/action"',
snippet='button "${label}" "${dest}"',
snippet='button "${1:Label}" "${2:/page/action.mu}"',
is_leaf=True)
# ---------------------------------------------------------------------------
@@ -222,17 +222,17 @@ register_keyword("button", node_class=FormButton, section="Form",
# ---------------------------------------------------------------------------
register_keyword("let", node_class=Let, section="Dynamic",
detail='let name = "value"', snippet='let ${name} = "${value}"',
detail='let name = "value"', snippet='let ${1:name} = "${2:value}"',
is_metadata=True)
register_keyword("source", node_class=Source, section="Dynamic",
detail='source name : shell "cmd"',
snippet='source ${name} : shell "${command}"',
snippet='source ${1:name} : shell "${2:command}"',
highlight_values=["shell", "file", "json", "python", "rns", "param"],
is_metadata=True)
register_keyword("if", node_class=IfBlock, section="Dynamic",
detail="if $var > threshold", snippet="if ${condition}",
detail="if $var > threshold", snippet="if ${1:condition}",
is_container=True)
register_keyword("elif", section="Dynamic",
@@ -242,20 +242,20 @@ register_keyword("else", section="Dynamic",
detail="", snippet="")
register_keyword("for", node_class=ForLoop, section="Dynamic",
detail="for item in $list", snippet='for ${var} in ${iterable}',
detail="for item in $list", snippet='for ${1:item} in ${2:\\$list}',
is_container=True)
register_keyword("cache", node_class=CacheControl, section="Dynamic",
detail="cache 0", snippet="cache ${seconds:0}",
detail="cache 0", snippet="cache ${1:0}",
is_metadata=True)
register_keyword("on_submit", node_class=OnSubmit, section="Dynamic",
detail='on_submit "form"', snippet='on_submit "${form_name}"',
detail='on_submit "form"', snippet='on_submit "${1:form_name}"',
is_container=True)
register_keyword("state", node_class=StateDecl, section="Dynamic",
detail='state "name" "/path.json"',
snippet='state "${name}" "${path}"',
snippet='state "${1:name}" "${2:/tmp/state.json}"',
is_metadata=True)
register_keyword("set", section="Dynamic", detail="", snippet="")

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 30 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 58 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 496 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 2.8 MiB

View File

@@ -315,40 +315,36 @@ export const EXAMPLES: Example[] = [
},
{
name: "Image Art",
description: "Convert images to braille/block/ascii character art",
source: `page "Gallery" 60
description: "Convert images to character art (upload an image first)",
source: `page "Image Embedding" 60
heading 1 "Image Embedding"
text "Upload an image with the Image button above,"
text "then use the image keyword to embed it."
spacer
row 2
col 26
image "backend/test_logo.png" braille 24
align center
caption "Braille mode"
col 30
heading 2 "Modes"
label "braille" "2x4 dot matrix"
label "block" "shade chars"
label "ascii" "classic ramp"
label "halfblock" "color blocks"
spacer
heading 2 "Options"
label "dither" "floyd | threshold"
label "invert" "flip light/dark"
box light "Syntax"
text 'image "path.png" mode width'
spacer
label "mode" "braille | block | ascii | halfblock"
label "width" "output width in characters"
spacer
image "backend/test_logo.png" block 24
align center
caption "Block mode"
heading 2 "Options"
label "dither" "floyd | threshold | none"
label "invert" "flip light and dark"
label "align" "left | center | right"
label "caption" "text below the image"
spacer
image "backend/test_logo.png" ascii 24
align center
caption "ASCII mode"`,
box rounded "Example"
text 'image "logo.png" braille 30'
text ' dither floyd'
text ' align center'
text ' caption "My logo"'`,
},
{
name: "Big Title",

View File

@@ -65,7 +65,6 @@ function getCommands(): CmdEntry[] {
* complete registry-driven list.
*/
export async function loadCommandsFromApi(): Promise<void> {
if (window.__uframeCommandsLoaded) return;
try {
const res = await fetch("/api/dsl-meta");

View File

@@ -14,24 +14,52 @@ import { tags } from "@lezer/highlight";
// Mutable sets — updated from /api/dsl-meta
let KEYWORDS = new Set([
// Fallback defaults (used before API response arrives)
// Layout
"page", "box", "row", "col", "spacer", "pad",
"heading", "text", "label", "divider", "link",
"list", "item", "gauge", "sparkline", "status",
"table", "columns", "form", "field", "radio",
"checkbox", "button", "source", "let", "if",
"elif", "else", "for", "align", "color", "bg",
"bold", "italic", "underline", "cache", "state",
"on_submit", "theme", "component", "use",
// Content
"heading", "text", "label", "divider", "link", "list", "item",
"bigtitle", "image", "dither", "invert", "caption",
// Data
"gauge", "sparkline", "status", "table", "columns",
// Navigation
"hnav", "vnav", "separator",
// Form
"form", "field", "radio", "checkbox", "button", "password",
// Style
"align", "color", "bg", "bold", "italic", "underline", "theme",
// Dynamic
"source", "let", "if", "elif", "else", "for", "cache", "state", "on_submit",
"set", "append", "prepend",
// Components
"component", "use",
]);
let WEIGHT_VALS = new Set([
// Border weights
"light", "heavy", "double", "rounded",
// List styles
"bullet", "dash", "number", "arrow",
// Alignment
"left", "center", "right",
"online", "offline", "degraded", "unknown",
// Status states
"online", "offline", "degraded", "unknown", "alert",
// Source types
"shell", "file", "json", "python", "rns", "param",
// Themes
"default", "nouveau", "gothic", "bamboo", "circuit", "brutalist",
// Nav styles
"bar", "tabs", "pills", "breadcrumb", "underline",
"boxed", "tree", "sidebar", "minimal",
// Fonts
"block", "thin", "pixel",
// Image modes
"braille", "ascii", "halfblock",
// Dither
"floyd", "threshold", "none",
// Divider styles
"dot",
// Misc
"active", "compact",
]);
/** Update keywords and values from /api/dsl-meta response. */

View File

@@ -1,11 +1,26 @@
import { useEffect, useState, type ReactNode } from "react";
import NavBar from "./NavBar";
import { useNavigate } from "react-router-dom";
import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner";
import frameSvg from "@/assets/frame.themed.svg";
const TITLE = `
▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄████████ ▄██████▄ ███▄▄▄▄ ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄██████▄ ███▄▄▄▄
▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███▀▀▀██▄
███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███
███ ███ ███ ███▌ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███
███ ███ ███ ███▌ ███ ▀▀███▀▀▀▀▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███
███ ███ ███ ███ ███ █▄ ▀███████████ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▄ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
▀█ ███ █▀ █▀ ████████▀ ███ ███ ▀██████▀ ▀█ █▀ ▀██████▀ ▀█ ███ █▀ █▀ ████████▀ ▀██████▀ ▀█ █▀
███ ███
`;
function toRoman(n: number): string {
const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1];
const syms = ["M","CM","D","CD","C","XC","L","XL","X","IX","V","IV","I"];
const vals = [1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1];
const syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
let r = "";
for (let i = 0; i < vals.length; i++) {
while (n >= vals[i]) { r += syms[i]; n -= vals[i]; }
@@ -28,15 +43,48 @@ function RomanClock() {
}
export default function AppShell({ children }: { children: ReactNode }) {
const navigate = useNavigate();
return (
<TooltipProvider>
<div className="flex flex-col h-screen bg-background text-foreground">
<NavBar />
<main className="flex-1 overflow-auto">{children}</main>
<main className="flex-1 overflow-auto">
<div className="relative max-w-5xl min-w-5xl mx-auto min-h-full">
{/* Frame SVG — background */}
<img
src={frameSvg}
alt=""
aria-hidden
className="absolute inset-0 w-full min-w-full pointer-events-none select-none"
style={{ zIndex: 0, objectFit: "fill" }}
/>
{/* Top hole — ASCII title */}
<div
className="relative z-10 flex flex-col cursor-pointer overflow-hidden"
style={{ paddingTop: 65, height: 185, paddingLeft: 60, paddingRight: 500, paddingBottom: 25 }}
onClick={() => navigate("/")}
>
<pre
className="select-none text-primary/70 hover:text-primary transition-colors whitespace-pre origin-center"
style={{ height: 60, fontSize: 6, lineHeight: 1.05, transform: "scale(0.50)", transformOrigin: "left center", alignContent: "center" }}
>
{TITLE}
</pre>
<span className="font-mono text-[10px] text-muted-foreground"><RomanClock /></span>
</div>
{/* Bottom hole — main content */}
<div
className="relative z-10 overflow-auto"
style={{ marginLeft: 12, marginRight: 68, width: 843, height: 1030, paddingTop: 8, paddingBottom: 20 }}
>
{children}
</div>
</div>
</main>
<footer className="flex items-center justify-center gap-3 text-[10px] text-muted-foreground py-4 shrink-0 tracking-widest uppercase">
<span>Built with hubris by the demiurge · MMXXVI</span>
<span className="text-muted-foreground/50"></span>
<span className="font-mono"><RomanClock /></span>
<span>Becoming with hubris · MMXXVI</span>
</footer>
</div>
<Toaster />

View File

@@ -1,46 +1,4 @@
import { useNavigate } from "react-router-dom";
import baldothSvg from "@/assets/baldoth.min.svg";
const TITLE = ` █████ ██ ██
██████ █████ █████ █
██ █ █ █████ █████ ███
█ █ █ █ ██ █ ██ █
█ █ █ █ ███ ████ ████ ████
██ ██ █ █ ███ ████ ████ ████ █ █ ███ ████ ████ █ ███ ████ ████ ████ ███ ████ █ ███ ████ ████
██ ██ █ █ ███ █ ███ █ ██ ████ █ ████ ████ ████ █ █ ████ ███ ████ ███ █ ███ █ ███ █ █ ████ ████ ████ █
██ ██ █ █ ██ █ ████ ██ ██ ██ ██ ████ ██ ██ ██ ████ ████ ██ █ ████ ██ ██ ██ ████
██ ██ █ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
█ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
█ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
████ █ ██ ██ ███ █ ███ ██████ ██ ██ ██████ ██ ██ ██ ██ ███ █ ██████ ██ ██
█ █████ ██ ███ █ ███████ ███ ████ ███ ███ ████ ███ ███ ███ ███ █ ███████ ████ ███ ███
█ ██ ███ █████ ███ ███ ███ ███ ███ ███ █████ ███ ███
██`;
// NavBar removed — frame.svg is the visual wrapper now
export default function NavBar() {
const navigate = useNavigate();
return (
<div className="flex flex-col items-center pt-6 pb-3 shrink-0 gap-2">
<button
onClick={() => navigate("/")}
className="hover:opacity-80 transition-opacity cursor-pointer flex flex-col items-center gap-2"
title="Go to Dashboard"
>
<div
className="h-28 w-28"
style={{
backgroundColor: "var(--primary)",
opacity: 0.5,
mask: `url(${baldothSvg}) center/contain no-repeat`,
WebkitMask: `url(${baldothSvg}) center/contain no-repeat`,
}}
/>
<pre className="text-[4px] leading-[1] tracking-tighter select-none text-primary/40">{TITLE}</pre>
</button>
</div>
);
return null;
}

View File

@@ -6,131 +6,133 @@
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-heading: var(--font-mono);
--font-sans: 'JetBrains Mono Variable', 'Courier New', monospace;
--font-mono: 'JetBrains Mono Variable', 'Courier New', monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-3xl: 0px;
--radius-4xl: 0px;
--font-heading: var(--font-mono);
--font-sans: 'JetBrains Mono Variable', 'Courier New', monospace;
--font-mono: 'JetBrains Mono Variable', 'Courier New', monospace;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar: var(--sidebar);
--color-chart-5: var(--chart-5);
--color-chart-4: var(--chart-4);
--color-chart-3: var(--chart-3);
--color-chart-2: var(--chart-2);
--color-chart-1: var(--chart-1);
--color-ring: var(--ring);
--color-input: var(--input);
--color-border: var(--border);
--color-destructive: var(--destructive);
--color-accent-foreground: var(--accent-foreground);
--color-accent: var(--accent);
--color-muted-foreground: var(--muted-foreground);
--color-muted: var(--muted);
--color-secondary-foreground: var(--secondary-foreground);
--color-secondary: var(--secondary);
--color-primary-foreground: var(--primary-foreground);
--color-primary: var(--primary);
--color-popover-foreground: var(--popover-foreground);
--color-popover: var(--popover);
--color-card-foreground: var(--card-foreground);
--color-card: var(--card);
--color-foreground: var(--foreground);
--color-background: var(--background);
--radius-sm: 0px;
--radius-md: 0px;
--radius-lg: 0px;
--radius-xl: 0px;
--radius-2xl: 0px;
--radius-3xl: 0px;
--radius-4xl: 0px;
}
/* ── Terracotta Light Theme ── */
:root {
--background: oklch(0.94 0.02 55);
--foreground: oklch(0.18 0.03 45);
--card: oklch(0.91 0.025 55);
--card-foreground: oklch(0.18 0.03 45);
--popover: oklch(0.91 0.025 55);
--popover-foreground: oklch(0.18 0.03 45);
--primary: oklch(0.55 0.14 45);
--primary-foreground: oklch(0.95 0.02 55);
--secondary: oklch(0.86 0.03 55);
--secondary-foreground: oklch(0.18 0.03 45);
--muted: oklch(0.86 0.025 55);
--muted-foreground: oklch(0.45 0.04 45);
--accent: oklch(0.84 0.035 55);
--accent-foreground: oklch(0.18 0.03 45);
--destructive: oklch(0.5 0.2 25);
--border: oklch(0.6 0.08 45);
--input: oklch(0.78 0.04 55);
--ring: oklch(0.55 0.14 45);
--chart-1: oklch(0.55 0.14 45);
--chart-2: oklch(0.65 0.1 70);
--chart-3: oklch(0.5 0.08 30);
--chart-4: oklch(0.6 0.06 90);
--chart-5: oklch(0.4 0.04 45);
--radius: 0px;
--sidebar: oklch(0.90 0.025 55);
--sidebar-foreground: oklch(0.18 0.03 45);
--sidebar-primary: oklch(0.55 0.14 45);
--sidebar-primary-foreground: oklch(0.95 0.02 55);
--sidebar-accent: oklch(0.84 0.035 55);
--sidebar-accent-foreground: oklch(0.18 0.03 45);
--sidebar-border: oklch(0.6 0.08 45);
--sidebar-ring: oklch(0.55 0.14 45);
--background: oklch(0.94 0.02 55);
--foreground: oklch(0.18 0.03 45);
--card: oklch(0.91 0.025 55);
--card-foreground: oklch(0.18 0.03 45);
--popover: oklch(0.91 0.025 55);
--popover-foreground: oklch(0.18 0.03 45);
--primary: oklch(0.55 0.14 45);
--primary-foreground: oklch(0.95 0.02 55);
--secondary: oklch(0.86 0.03 55);
--secondary-foreground: oklch(0.18 0.03 45);
--muted: oklch(0.86 0.025 55);
--muted-foreground: oklch(0.45 0.04 45);
--accent: oklch(0.84 0.035 55);
--accent-foreground: oklch(0.18 0.03 45);
--destructive: oklch(0.5 0.2 25);
--border: oklch(0.6 0.08 45);
--input: oklch(0.78 0.04 55);
--ring: oklch(0.55 0.14 45);
--chart-1: oklch(0.55 0.14 45);
--chart-2: oklch(0.65 0.1 70);
--chart-3: oklch(0.5 0.08 30);
--chart-4: oklch(0.6 0.06 90);
--chart-5: oklch(0.4 0.04 45);
--radius: 0px;
--sidebar: oklch(0.90 0.025 55);
--sidebar-foreground: oklch(0.18 0.03 45);
--sidebar-primary: oklch(0.55 0.14 45);
--sidebar-primary-foreground: oklch(0.95 0.02 55);
--sidebar-accent: oklch(0.84 0.035 55);
--sidebar-accent-foreground: oklch(0.18 0.03 45);
--sidebar-border: oklch(0.6 0.08 45);
--sidebar-ring: oklch(0.55 0.14 45);
}
/* ── Black-Figure Dark Theme (primary) ──
Inspired by Greek pottery: black ground, terracotta figures, gold accents
*/
.dark {
--background: oklch(0.12 0.015 45);
--foreground: oklch(0.72 0.08 55);
--card: oklch(0.16 0.02 45);
--card-foreground: oklch(0.72 0.08 55);
--popover: oklch(0.16 0.02 45);
--popover-foreground: oklch(0.72 0.08 55);
--primary: oklch(0.7 0.13 55);
--primary-foreground: oklch(0.12 0.015 45);
--secondary: oklch(0.22 0.025 45);
--secondary-foreground: oklch(0.7 0.08 55);
--muted: oklch(0.22 0.02 45);
--muted-foreground: oklch(0.5 0.05 55);
--accent: oklch(0.24 0.03 45);
--accent-foreground: oklch(0.72 0.08 55);
--destructive: oklch(0.55 0.18 25);
--border: oklch(0.38 0.06 45);
--input: oklch(0.26 0.03 45);
--ring: oklch(0.7 0.13 55);
--chart-1: oklch(0.7 0.13 55);
--chart-2: oklch(0.65 0.1 70);
--chart-3: oklch(0.55 0.08 30);
--chart-4: oklch(0.6 0.06 90);
--chart-5: oklch(0.45 0.04 45);
--sidebar: oklch(0.14 0.018 45);
--sidebar-foreground: oklch(0.72 0.08 55);
--sidebar-primary: oklch(0.7 0.13 55);
--sidebar-primary-foreground: oklch(0.12 0.015 45);
--sidebar-accent: oklch(0.24 0.03 45);
--sidebar-accent-foreground: oklch(0.72 0.08 55);
--sidebar-border: oklch(0.38 0.06 45);
--sidebar-ring: oklch(0.7 0.13 55);
--background: oklch(0.12 0.015 45);
--foreground: oklch(0.72 0.08 55);
--card: oklch(0.16 0.02 45);
--card-foreground: oklch(0.72 0.08 55);
--popover: oklch(0.16 0.02 45);
--popover-foreground: oklch(0.72 0.08 55);
--primary: oklch(0.7 0.13 55);
--primary-foreground: oklch(0.12 0.015 45);
--secondary: oklch(0.22 0.025 45);
--secondary-foreground: oklch(0.7 0.08 55);
--muted: oklch(0.22 0.02 45);
--muted-foreground: oklch(0.5 0.05 55);
--accent: oklch(0.24 0.03 45);
--accent-foreground: oklch(0.72 0.08 55);
--destructive: oklch(0.55 0.18 25);
--border: oklch(0.38 0.06 45);
--input: oklch(0.26 0.03 45);
--ring: oklch(0.7 0.13 55);
--chart-1: oklch(0.7 0.13 55);
--chart-2: oklch(0.65 0.1 70);
--chart-3: oklch(0.55 0.08 30);
--chart-4: oklch(0.6 0.06 90);
--chart-5: oklch(0.45 0.04 45);
--sidebar: oklch(0.14 0.018 45);
--sidebar-foreground: oklch(0.72 0.08 55);
--sidebar-primary: oklch(0.7 0.13 55);
--sidebar-primary-foreground: oklch(0.12 0.015 45);
--sidebar-accent: oklch(0.24 0.03 45);
--sidebar-accent-foreground: oklch(0.72 0.08 55);
--sidebar-border: oklch(0.38 0.06 45);
--sidebar-ring: oklch(0.7 0.13 55);
}
@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
font-size: 13px;
letter-spacing: -0.01em;
}
html {
@apply font-mono;
}
@@ -140,20 +142,25 @@
scrollbar-width: thin;
scrollbar-color: oklch(0.35 0.05 45) transparent;
}
*::-webkit-scrollbar {
width: 5px;
height: 5px;
}
*::-webkit-scrollbar-track {
background: transparent;
}
*::-webkit-scrollbar-thumb {
background: oklch(0.35 0.05 45);
border-radius: 0;
}
*::-webkit-scrollbar-thumb:hover {
background: oklch(0.5 0.08 55);
}
*::-webkit-scrollbar-corner {
background: transparent;
}
@@ -164,13 +171,19 @@
}
/* Bold console aesthetic */
button, [role="button"], [data-slot="button"] {
button,
[role="button"],
[data-slot="button"] {
text-transform: uppercase;
letter-spacing: 0.05em;
font-size: 11px;
cursor: pointer;
}
.cm-content {
background: var(--background);
}
/* Keyword hover tooltip */
.cm-tooltip.cm-tooltip-hover {
background: var(--card) !important;
@@ -185,9 +198,14 @@
}
/* Ensure all interactive elements have pointer cursor */
a, [role="link"], [role="tab"], [role="option"],
[data-slot="popover-trigger"], [data-slot="toggle-group-item"],
[data-slot="alert-dialog-action"], [data-slot="alert-dialog-cancel"],
a,
[role="link"],
[role="tab"],
[role="option"],
[data-slot="popover-trigger"],
[data-slot="toggle-group-item"],
[data-slot="alert-dialog-action"],
[data-slot="alert-dialog-cancel"],
.cm-tooltip-autocomplete [role="option"] {
cursor: pointer;
}
@@ -238,6 +256,7 @@
6px 4px 0 0 var(--border);
transition: box-shadow 0.1s, transform 0.1s;
}
button[data-slot="button"][data-variant="default"]:active {
box-shadow: none;
transform: translate(2px, 2px);
@@ -256,4 +275,4 @@
input[data-slot="input"] {
border: 2px solid var(--border);
}
}
}

View File

@@ -103,8 +103,8 @@ export default function DashboardView() {
);
return (
<div className="max-w-5xl mx-auto px-4">
<div className="border-2 border-border" style={{boxShadow:'3px 3px 0 0 var(--border),5px 3px 0 0 transparent,7px 3px 0 0 var(--border),4px 4px 0 0 transparent,6px 4px 0 0 var(--border),3px 5px 0 0 var(--border),5px 5px 0 0 transparent,7px 5px 0 0 var(--border),4px 6px 0 0 var(--border),6px 6px 0 0 transparent'}}>
<div>
<div>
{/* Header row */}
<div className="flex items-center px-4 py-2 border-b-2 border-border">
<h1 className="text-sm font-semibold flex-1">Pages</h1>

View File

@@ -142,8 +142,8 @@ export default function EditorView() {
}, [handleSave]);
return (
<div className="flex flex-col h-full max-w-5xl mx-auto w-full px-4">
<div className="flex flex-col flex-1 border-2 border-border min-h-0" style={{boxShadow:'3px 3px 0 0 var(--border),5px 3px 0 0 transparent,7px 3px 0 0 var(--border),4px 4px 0 0 transparent,6px 4px 0 0 var(--border),3px 5px 0 0 var(--border),5px 5px 0 0 transparent,7px 5px 0 0 var(--border),4px 6px 0 0 var(--border),6px 6px 0 0 transparent'}}>
<div className="flex flex-col h-full">
<div className="flex flex-col flex-1 min-h-0">
<ToolBar
pageName={pageName}
onNameChange={isNew ? setPageName : undefined}