feat(ui): redesign UI with Tailwind CSS, CodeMirror 6, and new layout

- Replace all hand-written CSS with Tailwind utility classes
- Upgrade CodeMirror 5 to CodeMirror 6 (scoped @codemirror/* packages)
- Redesign layout with responsive sidebar navigation using JSX/Mithril
- Flatten admin routes (e.g. /admin/presets -> /presets)
- Add "remember me" login option with 2-week persistent cookie
- Add delete confirmation dialogs across admin pages
- Upgrade esbuild from 0.21 to 0.27 and update browser targets
- Add Inter and Roboto Mono variable fonts with subset script
This commit is contained in:
Zaid Humaimidi committed 2026-03-12 14:55:10 -07:00
1 parent a7256620e0
commit 4a191afdec
64 files changed
+4147 -2421

No files matched your search

+50 -25
View File
@@ -1,4 +1,3 @@
import { Editor } from "codemirror";
import { ClosureComponent } from "mithril";
import { m } from "./components.ts";
import { codeMirror } from "./dynamic-loader.ts";
@@ -11,41 +10,67 @@ interface Attrs {
focus?: boolean;
onSubmit?: (dom: Element) => void;
onChange?: (value: string) => void;
onReady?: (editor: Editor) => void;
}
const component: ClosureComponent<Attrs> = () => {
return {
view: (vnode) => {
return m("textarea", {
name: vnode.attrs.id,
value: vnode.attrs.value,
return m("div.font-mono text-sm", {
oncreate: (_vnode) => {
const editor = codeMirror.fromTextArea(
_vnode.dom as HTMLTextAreaElement,
{
mode: vnode.attrs.mode,
lineNumbers: true,
readOnly: vnode.attrs.readOnly,
extraKeys: {
"Ctrl-Enter": () => {
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
},
"Cmd-Enter": () => {
if (vnode.attrs.onSubmit) vnode.attrs.onSubmit(_vnode.dom);
},
const theme = codeMirror.EditorView.theme({
"&.cm-editor": {
display: "block",
width: "50rem",
height: "30rem",
"max-width": "100%",
"border-radius": "0.375rem",
"border-width": "1px",
"border-color": "var(--color-stone-300)",
"box-shadow":
"var(--tw-ring-shadow, 0 0 #0000), 0 1px 2px 0 rgb(0 0 0 / 0.05)",
overflow: "hidden",
"& > .cm-scroller": {
"font-family": "inherit",
"line-height": "inherit",
},
},
);
"&.cm-editor.cm-focused": {
outline: "none",
"border-color": "var(--color-cyan-500)",
"--tw-ring-shadow": "0 0 0 1px var(--color-cyan-500)",
},
});
if (vnode.attrs.onChange) {
editor.on("change", (e) => {
vnode.attrs.onChange(e.getValue());
});
}
const extensions = [
theme,
codeMirror.lineNumbers(),
codeMirror.history(),
codeMirror.syntaxHighlighting(codeMirror.defaultHighlightStyle),
codeMirror.keymap.of([
...codeMirror.defaultKeymap,
...codeMirror.historyKeymap,
]),
codeMirror.EditorState.readOnly.of(!!vnode.attrs.readOnly),
codeMirror.EditorView.updateListener.of((update) => {
if (update.docChanged && vnode.attrs.onChange)
vnode.attrs.onChange(update.state.doc.toString());
}),
];
if (vnode.attrs.mode === "javascript")
extensions.push(codeMirror.javascript());
else if (vnode.attrs.mode === "yaml")
extensions.push(codeMirror.yaml());
const editor = new codeMirror.EditorView({
state: codeMirror.EditorState.create({
doc: vnode.attrs.value,
extensions,
}),
parent: _vnode.dom as HTMLTextAreaElement,
});
if (vnode.attrs.focus) editor.focus();
if (vnode.attrs.onReady) vnode.attrs.onReady(editor);
},
});
},