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", 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) is_container=True)
register_keyword("box", node_class=Box, section="Layout", 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"], highlight_values=["light", "heavy", "double", "rounded"],
is_container=True) is_container=True)
register_keyword("row", node_class=Row, section="Layout", 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) is_container=True)
register_keyword("col", node_class=Col, section="Layout", register_keyword("col", node_class=Col, section="Layout",
detail="col [width]", snippet="col ${width}", detail="col [width]", snippet="col ${1}",
is_container=True) is_container=True)
register_keyword("spacer", node_class=Spacer, section="Layout", register_keyword("spacer", node_class=Spacer, section="Layout",
@@ -51,7 +51,7 @@ register_keyword("spacer", node_class=Spacer, section="Layout",
is_leaf=True) is_leaf=True)
register_keyword("pad", node_class=Pad, section="Layout", 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) is_container=True)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -59,28 +59,28 @@ register_keyword("pad", node_class=Pad, section="Layout",
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_keyword("heading", node_class=Heading, section="Content", 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) is_leaf=True)
register_keyword("text", node_class=Text, section="Content", register_keyword("text", node_class=Text, section="Content",
detail='text "Content"', snippet='text "${content}"', detail='text "Content"', snippet='text "${1:Content}"',
is_leaf=True) is_leaf=True)
register_keyword("label", node_class=Label, section="Content", 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) is_leaf=True)
register_keyword("divider", node_class=Divider, section="Content", 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"], highlight_values=["light", "heavy", "double", "dash", "dot"],
is_leaf=True) is_leaf=True)
register_keyword("link", node_class=Link, section="Content", 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) is_leaf=True)
register_keyword("list", node_class=ListNode, section="Content", 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"], highlight_values=["bullet", "dash", "number", "arrow"],
is_container=True) is_container=True)
@@ -94,17 +94,17 @@ register_keyword("item", node_class=ListItem, section="",
register_keyword("gauge", node_class=Gauge, section="Data", register_keyword("gauge", node_class=Gauge, section="Data",
detail="gauge label val max width", 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) is_leaf=True)
register_keyword("sparkline", node_class=Sparkline, section="Data", register_keyword("sparkline", node_class=Sparkline, section="Data",
detail="sparkline label values width", 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) is_leaf=True)
register_keyword("status", node_class=Status, section="Data", register_keyword("status", node_class=Status, section="Data",
detail="status label state", detail="status label state",
snippet='status "${label}" ${state:online}', snippet='status "${1:Label}" ${2:online}',
highlight_values=["online", "offline", "degraded", "unknown", "alert"], highlight_values=["online", "offline", "degraded", "unknown", "alert"],
is_leaf=True) is_leaf=True)
@@ -121,18 +121,18 @@ register_keyword("columns", section="",
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_keyword("hnav", node_class=HNav, section="Layout", 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"], highlight_values=["bar", "tabs", "pills", "breadcrumb", "underline"],
is_container=True) is_container=True)
register_keyword("vnav", node_class=VNav, section="Layout", 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"], highlight_values=["list", "boxed", "tree", "sidebar", "minimal"],
is_container=True) is_container=True)
register_keyword("image", node_class=ImageNode, section="Content", register_keyword("image", node_class=ImageNode, section="Content",
detail='image "path.png" braille 30', 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"], highlight_values=["braille", "block", "ascii", "halfblock"],
is_leaf=True) is_leaf=True)
@@ -151,7 +151,7 @@ register_keyword("caption", section="",
register_keyword("bigtitle", node_class=BigTitle, section="Content", register_keyword("bigtitle", node_class=BigTitle, section="Content",
detail='bigtitle "TEXT" block', detail='bigtitle "TEXT" block',
snippet='bigtitle "${text}" ${font:block}', snippet='bigtitle "${1:TEXT}" ${2:block}',
highlight_values=["block", "thin", "pixel"], highlight_values=["block", "thin", "pixel"],
is_leaf=True) is_leaf=True)
@@ -160,12 +160,12 @@ register_keyword("bigtitle", node_class=BigTitle, section="Content",
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_keyword("align", section="Style", register_keyword("align", section="Style",
detail="align center", snippet="align ${align:center}", detail="align center", snippet="align ${1:center}",
highlight_values=["left", "center", "right"], highlight_values=["left", "center", "right"],
is_style_directive=True) is_style_directive=True)
register_keyword("color", section="Style", register_keyword("color", section="Style",
detail="color hex", snippet="color ${hex}", detail="color hex", snippet="color ${1:0cf}",
is_style_directive=True) is_style_directive=True)
register_keyword("bg", section="Style", register_keyword("bg", section="Style",
@@ -189,32 +189,32 @@ register_keyword("underline", section="Style",
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_keyword("form", node_class=Form, section="Form", register_keyword("form", node_class=Form, section="Form",
detail='form "name"', snippet='form "${name}"', detail='form "name"', snippet='form "${1:name}"',
is_container=True) is_container=True)
register_keyword("field", node_class=Field, section="Form", register_keyword("field", node_class=Field, section="Form",
detail='field "name" 24 "placeholder"', detail='field "name" 24 "placeholder"',
snippet='field "${name}" ${width:24} "${placeholder}"', snippet='field "${1:name}" ${2:24} "${3:placeholder}"',
is_leaf=True) is_leaf=True)
register_keyword("password", node_class=Password, section="Form", register_keyword("password", node_class=Password, section="Form",
detail='password "name" 24 "placeholder"', detail='password "name" 24 "placeholder"',
snippet='password "${name}" ${width:24} "${placeholder}"', snippet='password "${1:name}" ${2:24} "${3:placeholder}"',
is_leaf=True) is_leaf=True)
register_keyword("radio", node_class=Radio, section="Form", register_keyword("radio", node_class=Radio, section="Form",
detail='radio "group" "A" | "B" | "C"', detail='radio "group" "A" | "B" | "C"',
snippet='radio "${group}" "${opt1}" | "${opt2}"', snippet='radio "${1:group}" "${2:Option A}" | "${3:Option B}"',
is_leaf=True) is_leaf=True)
register_keyword("checkbox", node_class=Checkbox, section="Form", register_keyword("checkbox", node_class=Checkbox, section="Form",
detail='checkbox "name" "Label"', detail='checkbox "name" "Label"',
snippet='checkbox "${name}" "${label}"', snippet='checkbox "${1:name}" "${2:Label}"',
is_leaf=True) is_leaf=True)
register_keyword("button", node_class=FormButton, section="Form", register_keyword("button", node_class=FormButton, section="Form",
detail='button "Label" "/action"', detail='button "Label" "/action"',
snippet='button "${label}" "${dest}"', snippet='button "${1:Label}" "${2:/page/action.mu}"',
is_leaf=True) is_leaf=True)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -222,17 +222,17 @@ register_keyword("button", node_class=FormButton, section="Form",
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
register_keyword("let", node_class=Let, section="Dynamic", 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) is_metadata=True)
register_keyword("source", node_class=Source, section="Dynamic", register_keyword("source", node_class=Source, section="Dynamic",
detail='source name : shell "cmd"', 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"], highlight_values=["shell", "file", "json", "python", "rns", "param"],
is_metadata=True) is_metadata=True)
register_keyword("if", node_class=IfBlock, section="Dynamic", 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) is_container=True)
register_keyword("elif", section="Dynamic", register_keyword("elif", section="Dynamic",
@@ -242,20 +242,20 @@ register_keyword("else", section="Dynamic",
detail="", snippet="") detail="", snippet="")
register_keyword("for", node_class=ForLoop, section="Dynamic", 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) is_container=True)
register_keyword("cache", node_class=CacheControl, section="Dynamic", 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) is_metadata=True)
register_keyword("on_submit", node_class=OnSubmit, section="Dynamic", 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) is_container=True)
register_keyword("state", node_class=StateDecl, section="Dynamic", register_keyword("state", node_class=StateDecl, section="Dynamic",
detail='state "name" "/path.json"', detail='state "name" "/path.json"',
snippet='state "${name}" "${path}"', snippet='state "${1:name}" "${2:/tmp/state.json}"',
is_metadata=True) is_metadata=True)
register_keyword("set", section="Dynamic", detail="", snippet="") 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", name: "Image Art",
description: "Convert images to braille/block/ascii character art", description: "Convert images to character art (upload an image first)",
source: `page "Gallery" 60 source: `page "Image Embedding" 60
heading 1 "Image Embedding" heading 1 "Image Embedding"
text "Upload an image with the Image button above,"
text "then use the image keyword to embed it."
spacer spacer
row 2 box light "Syntax"
col 26 text 'image "path.png" mode width'
image "backend/test_logo.png" braille 24 spacer
align center label "mode" "braille | block | ascii | halfblock"
caption "Braille mode" label "width" "output width in characters"
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"
spacer spacer
image "backend/test_logo.png" block 24 heading 2 "Options"
align center label "dither" "floyd | threshold | none"
caption "Block mode" label "invert" "flip light and dark"
label "align" "left | center | right"
label "caption" "text below the image"
spacer spacer
image "backend/test_logo.png" ascii 24 box rounded "Example"
align center text 'image "logo.png" braille 30'
caption "ASCII mode"`, text ' dither floyd'
text ' align center'
text ' caption "My logo"'`,
}, },
{ {
name: "Big Title", name: "Big Title",

View File

@@ -65,7 +65,6 @@ function getCommands(): CmdEntry[] {
* complete registry-driven list. * complete registry-driven list.
*/ */
export async function loadCommandsFromApi(): Promise<void> { export async function loadCommandsFromApi(): Promise<void> {
if (window.__uframeCommandsLoaded) return;
try { try {
const res = await fetch("/api/dsl-meta"); 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 // Mutable sets — updated from /api/dsl-meta
let KEYWORDS = new Set([ let KEYWORDS = new Set([
// Fallback defaults (used before API response arrives) // Layout
"page", "box", "row", "col", "spacer", "pad", "page", "box", "row", "col", "spacer", "pad",
"heading", "text", "label", "divider", "link", // Content
"list", "item", "gauge", "sparkline", "status", "heading", "text", "label", "divider", "link", "list", "item",
"table", "columns", "form", "field", "radio", "bigtitle", "image", "dither", "invert", "caption",
"checkbox", "button", "source", "let", "if", // Data
"elif", "else", "for", "align", "color", "bg", "gauge", "sparkline", "status", "table", "columns",
"bold", "italic", "underline", "cache", "state", // Navigation
"on_submit", "theme", "component", "use", "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([ let WEIGHT_VALS = new Set([
// Border weights
"light", "heavy", "double", "rounded", "light", "heavy", "double", "rounded",
// List styles
"bullet", "dash", "number", "arrow", "bullet", "dash", "number", "arrow",
// Alignment
"left", "center", "right", "left", "center", "right",
"online", "offline", "degraded", "unknown", // Status states
"online", "offline", "degraded", "unknown", "alert",
// Source types
"shell", "file", "json", "python", "rns", "param", "shell", "file", "json", "python", "rns", "param",
// Themes
"default", "nouveau", "gothic", "bamboo", "circuit", "brutalist", "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. */ /** Update keywords and values from /api/dsl-meta response. */

View File

@@ -1,11 +1,26 @@
import { useEffect, useState, type ReactNode } from "react"; import { useEffect, useState, type ReactNode } from "react";
import NavBar from "./NavBar"; import { useNavigate } from "react-router-dom";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import frameSvg from "@/assets/frame.themed.svg";
const TITLE = `
▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄████████ ▄██████▄ ███▄▄▄▄ ▄██████▄ ▄▄▄▄███▄▄▄▄ ▄█ ▄████████ ▄██████▄ ███▄▄▄▄
▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███ ███ ███▀▀▀██▄ ███ ███ ▄██▀▀▀███▀▀▀██▄ ███ ███ ███ ███ ███ ███▀▀▀██▄
███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ █▀ ███ ███ ███ ███
███ ███ ███ ███▌ ███ ▄███▄▄▄▄██▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███
███ ███ ███ ███▌ ███ ▀▀███▀▀▀▀▀ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███▌ ███ ███ ███ ███ ███
███ ███ ███ ███ ███ █▄ ▀███████████ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ █▄ ███ ███ ███ ███
███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███ ███
▀█ ███ █▀ █▀ ████████▀ ███ ███ ▀██████▀ ▀█ █▀ ▀██████▀ ▀█ ███ █▀ █▀ ████████▀ ▀██████▀ ▀█ █▀
███ ███
`;
function toRoman(n: number): string { function toRoman(n: number): string {
const vals = [1000,900,500,400,100,90,50,40,10,9,5,4,1]; 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 syms = ["M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"];
let r = ""; let r = "";
for (let i = 0; i < vals.length; i++) { for (let i = 0; i < vals.length; i++) {
while (n >= vals[i]) { r += syms[i]; n -= vals[i]; } while (n >= vals[i]) { r += syms[i]; n -= vals[i]; }
@@ -28,15 +43,48 @@ function RomanClock() {
} }
export default function AppShell({ children }: { children: ReactNode }) { export default function AppShell({ children }: { children: ReactNode }) {
const navigate = useNavigate();
return ( return (
<TooltipProvider> <TooltipProvider>
<div className="flex flex-col h-screen bg-background text-foreground"> <div className="flex flex-col h-screen bg-background text-foreground">
<NavBar /> <main className="flex-1 overflow-auto">
<main className="flex-1 overflow-auto">{children}</main> <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"> <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>Becoming with hubris · MMXXVI</span>
<span className="text-muted-foreground/50"></span>
<span className="font-mono"><RomanClock /></span>
</footer> </footer>
</div> </div>
<Toaster /> <Toaster />

View File

@@ -1,46 +1,4 @@
import { useNavigate } from "react-router-dom"; // NavBar removed — frame.svg is the visual wrapper now
import baldothSvg from "@/assets/baldoth.min.svg";
const TITLE = ` █████ ██ ██
██████ █████ █████ █
██ █ █ █████ █████ ███
█ █ █ █ ██ █ ██ █
█ █ █ █ ███ ████ ████ ████
██ ██ █ █ ███ ████ ████ ████ █ █ ███ ████ ████ █ ███ ████ ████ ████ ███ ████ █ ███ ████ ████
██ ██ █ █ ███ █ ███ █ ██ ████ █ ████ ████ ████ █ █ ████ ███ ████ ███ █ ███ █ ███ █ █ ████ ████ ████ █
██ ██ █ █ ██ █ ████ ██ ██ ██ ██ ████ ██ ██ ██ ████ ████ ██ █ ████ ██ ██ ██ ████
██ ██ █ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
██ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
█ ██ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
█ █ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██
████ █ ██ ██ ███ █ ███ ██████ ██ ██ ██████ ██ ██ ██ ██ ███ █ ██████ ██ ██
█ █████ ██ ███ █ ███████ ███ ████ ███ ███ ████ ███ ███ ███ ███ █ ███████ ████ ███ ███
█ ██ ███ █████ ███ ███ ███ ███ ███ ███ █████ ███ ███
██`;
export default function NavBar() { export default function NavBar() {
const navigate = useNavigate(); return null;
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>
);
} }

View File

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

View File

@@ -103,8 +103,8 @@ export default function DashboardView() {
); );
return ( return (
<div className="max-w-5xl mx-auto px-4"> <div>
<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>
{/* Header row */} {/* Header row */}
<div className="flex items-center px-4 py-2 border-b-2 border-border"> <div className="flex items-center px-4 py-2 border-b-2 border-border">
<h1 className="text-sm font-semibold flex-1">Pages</h1> <h1 className="text-sm font-semibold flex-1">Pages</h1>

View File

@@ -142,8 +142,8 @@ export default function EditorView() {
}, [handleSave]); }, [handleSave]);
return ( return (
<div className="flex flex-col h-full max-w-5xl mx-auto w-full px-4"> <div className="flex flex-col h-full">
<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 flex-1 min-h-0">
<ToolBar <ToolBar
pageName={pageName} pageName={pageName}
onNameChange={isNew ? setPageName : undefined} onNameChange={isNew ? setPageName : undefined}