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:
1 parent
a7256620e0
commit
4a191afdec
64 files changed
+4147
-2421
No files matched your search
@@ -0,0 +1,200 @@
|
||||
import m, {
|
||||
ChildArrayOrPrimitive,
|
||||
ClosureComponent,
|
||||
mount,
|
||||
Vnode,
|
||||
VnodeDOM,
|
||||
} from "mithril";
|
||||
import { ICONS_SVG } from "../build/assets.ts";
|
||||
|
||||
export const portal: ClosureComponent<void> = () => {
|
||||
let rootElement: HTMLElement;
|
||||
let children: ChildArrayOrPrimitive;
|
||||
|
||||
return {
|
||||
oncreate: (vnode) => {
|
||||
children = vnode.children;
|
||||
rootElement = document.createElement("div");
|
||||
document.body.appendChild(rootElement);
|
||||
mount(rootElement, { view: () => children });
|
||||
},
|
||||
|
||||
onupdate: (vnode) => {
|
||||
children = vnode.children;
|
||||
},
|
||||
|
||||
onremove: () => {
|
||||
if (document.body.contains(rootElement)) {
|
||||
mount(rootElement, null);
|
||||
document.body.removeChild(rootElement);
|
||||
}
|
||||
},
|
||||
|
||||
view: () => {
|
||||
return null;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface DialogAttrs {
|
||||
onClose?: () => void;
|
||||
as: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const dialog: ClosureComponent<DialogAttrs> = () => {
|
||||
return {
|
||||
view(vnode) {
|
||||
return m(
|
||||
portal,
|
||||
m(
|
||||
vnode.attrs.as,
|
||||
{ class: vnode.attrs.class, role: "dialog", "aria-modal": "true" },
|
||||
vnode.children,
|
||||
),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface DialogOverlayAttrs {
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const dialogOverlay: ClosureComponent<DialogOverlayAttrs> = () => {
|
||||
return {
|
||||
view(vnode) {
|
||||
return m(
|
||||
"div",
|
||||
{ class: vnode.attrs.class, "aria-hidden": "true" },
|
||||
vnode.children,
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
let transitionShow = false;
|
||||
let transitionTimeout = 0;
|
||||
|
||||
interface TransitionRootAttrs {
|
||||
show: boolean;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export const transitionRoot: ClosureComponent<TransitionRootAttrs> = (
|
||||
initialVnode,
|
||||
) => {
|
||||
let show = initialVnode.attrs.show;
|
||||
let transitionTimestamp = 0;
|
||||
let timeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
return {
|
||||
view(vnode: Vnode<TransitionRootAttrs>) {
|
||||
if (show !== vnode.attrs.show) {
|
||||
const now = Date.now();
|
||||
transitionTimestamp = now;
|
||||
show = vnode.attrs.show;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => m.redraw(), vnode.attrs.duration);
|
||||
}
|
||||
|
||||
transitionShow = show;
|
||||
transitionTimeout = Math.max(
|
||||
0,
|
||||
vnode.attrs.duration - (Date.now() - transitionTimestamp),
|
||||
);
|
||||
|
||||
if (!transitionShow && !transitionTimeout) return null;
|
||||
|
||||
return vnode.children;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface TransitionChildAttrs {
|
||||
enter: string;
|
||||
enterFrom: string;
|
||||
enterTo: string;
|
||||
leave: string;
|
||||
leaveFrom: string;
|
||||
leaveTo: string;
|
||||
}
|
||||
|
||||
export const transitionChild: ClosureComponent<TransitionChildAttrs> = () => {
|
||||
let show: boolean;
|
||||
let timeout: number;
|
||||
let firstFrame: boolean;
|
||||
|
||||
function updateCssClasses(vnode: VnodeDOM<TransitionChildAttrs>): void {
|
||||
const dom = vnode.dom as HTMLElement;
|
||||
const enter = vnode.attrs.enter.split(" ");
|
||||
const enterFrom = vnode.attrs.enterFrom.split(" ");
|
||||
const enterTo = vnode.attrs.enterTo.split(" ");
|
||||
const leave = vnode.attrs.leave.split(" ");
|
||||
const leaveFrom = vnode.attrs.leaveFrom.split(" ");
|
||||
const leaveTo = vnode.attrs.leaveTo.split(" ");
|
||||
|
||||
if (show) {
|
||||
dom.classList.remove(...leave, ...leaveFrom, ...leaveTo);
|
||||
if (firstFrame) {
|
||||
dom.classList.add(...enterFrom);
|
||||
void dom.getBoundingClientRect();
|
||||
dom.classList.remove(...enterFrom);
|
||||
}
|
||||
if (timeout) dom.classList.add(...enter);
|
||||
else dom.classList.remove(...enter);
|
||||
dom.classList.add(...enterTo);
|
||||
} else {
|
||||
dom.classList.remove(...enter, ...enterFrom, ...enterTo);
|
||||
if (firstFrame) {
|
||||
dom.classList.add(...leaveFrom);
|
||||
void dom.getBoundingClientRect();
|
||||
dom.classList.remove(...leaveFrom);
|
||||
}
|
||||
if (timeout) dom.classList.add(...leave);
|
||||
else dom.classList.remove(...leave);
|
||||
dom.classList.add(...leaveTo);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
view(vnode: Vnode<TransitionChildAttrs>) {
|
||||
firstFrame = show !== transitionShow;
|
||||
show = transitionShow;
|
||||
timeout = transitionTimeout;
|
||||
return vnode.children;
|
||||
},
|
||||
|
||||
oncreate(vnode: VnodeDOM<TransitionChildAttrs>) {
|
||||
updateCssClasses(vnode);
|
||||
},
|
||||
|
||||
onupdate(vnode: VnodeDOM<TransitionChildAttrs>) {
|
||||
updateCssClasses(vnode);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
interface IconAttrs {
|
||||
name: string;
|
||||
class?: string;
|
||||
}
|
||||
|
||||
export const icon: ClosureComponent<IconAttrs> = () => {
|
||||
return {
|
||||
view(vnode) {
|
||||
return m(
|
||||
`svg`,
|
||||
{
|
||||
xmlns: "http://www.w3.org/2000/svg",
|
||||
fill: "none",
|
||||
stroke: "currentColor",
|
||||
"stroke-width": "2",
|
||||
class: vnode.attrs.class,
|
||||
"aria-hidden": "true",
|
||||
},
|
||||
m("use", { href: `${ICONS_SVG}#icon-${vnode.attrs.name}` }),
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in new issue
Block a user