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

+88 -81
View File
@@ -1,4 +1,5 @@
import m, { ClosureComponent, Component } from "mithril";
import { ClosureComponent, Component } from "mithril";
import { m } from "./components.ts";
import * as notifications from "./notifications.ts";
export async function init(): Promise<Record<string, unknown>> {
@@ -14,93 +15,99 @@ export const component: ClosureComponent = (vnode): Component => {
view: () => {
document.title = "Initialization wizard - GenieACS";
const checkboxes = [
"users",
"presets",
"filters",
"device",
"index",
"overview",
].map((s) => {
if (!options[s]) selected.delete(s);
return m("input", {
type: "checkbox",
checked: selected.has(s),
disabled: !options[s],
style: "display: inline; margin-right: 0.5em;",
onclick: (e) => {
if (e.target.checked) selected.add(s);
else selected.delete(s);
},
});
});
const items = [
{ key: "users", label: "Users, roles and permissions" },
{ key: "presets", label: "Presets and provisions" },
{ key: "filters", label: "Devices predefined search filters" },
{ key: "device", label: "Device details page" },
{ key: "index", label: "Devices listing page" },
{ key: "overview", label: "Overview page" },
];
return m(".wizard-dialog", [
m("h1", "Initialization wizard"),
return [
m(
"p",
"This wizard will seed the database with a minimal initial configuration to serve as a starting point. Select what you want to initialize and click 'ABRACADABRA!'.",
"h1.text-xl font-medium text-stone-900 mb-5",
"Initialization wizard",
),
m("div", m("label", checkboxes[0], "Users, roles and permissions")),
m("div", m("label", checkboxes[1], "Presets and provisions")),
m(
"div",
m("label", checkboxes[2], "Devices predefined search filters"),
),
m("div", m("label", checkboxes[3], "Device details page")),
m("div", m("label", checkboxes[4], "Devices listing page")),
m("div", m("label", checkboxes[5], "Overview page")),
m(
"button.primary",
{
style: "margin: 10px;",
disabled: selected.size === 0,
onclick: (e) => {
e.target.disabled = true;
m(".bg-white shadow-sm rounded-lg p-6 sm:p-8 max-w-lg", [
m(
"p.text-sm text-stone-600 mb-6",
"This wizard will seed the database with a minimal initial configuration to serve as a starting point. Select what you want to initialize and click the button below.",
),
m(
".flex flex-col gap-3 mb-6",
items.map((item) => {
if (!options[item.key]) selected.delete(item.key);
return m(
"label.flex items-center text-sm text-stone-700",
{ class: options[item.key] ? "" : "opacity-50" },
m(
"input.focus:ring-cyan-500 h-4 w-4 text-cyan-700 border-stone-300 rounded-sm",
{
type: "checkbox",
checked: selected.has(item.key),
disabled: !options[item.key],
onclick: (e) => {
if (e.target.checked) selected.add(item.key);
else selected.delete(item.key);
},
},
),
m("span.ml-2", item.label),
);
}),
),
m(
"button.inline-flex justify-center py-2 px-4 border border-transparent shadow-xs text-sm font-medium rounded-md text-white bg-cyan-600 hover:bg-cyan-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-cyan-500 disabled:opacity-50 disabled:cursor-not-allowed",
{
disabled: selected.size === 0,
onclick: (e) => {
e.target.disabled = true;
const opts = {};
for (const s of selected) opts[s] = true;
const opts = {};
for (const s of selected) opts[s] = true;
m.request({
method: "POST",
url: "init",
body: opts,
})
.then(() => {
setTimeout(() => {
m.request({ url: "init" })
.then((o) => {
e.target.disabled = false;
options = o;
notifications.push(
"success",
"Initialization complete",
{
"Open Sesame!": () => {
m.route.set("/login");
window.location.reload();
},
},
);
})
.catch((err) => {
notifications.push("error", err.message);
});
}, 3000);
if (opts["users"]) {
alert(
"An administrator user has been created for you. Use admin/admin to log in. Don't forget to change the default password.",
);
}
m.request({
method: "POST",
url: "init",
body: opts,
})
.catch((err) => {
notifications.push("error", err.message);
});
.then(() => {
setTimeout(() => {
m.request({ url: "init" })
.then((o) => {
e.target.disabled = false;
options = o;
notifications.push(
"success",
"Initialization complete",
{
"Open Sesame!": () => {
m.route.set("/login");
window.location.reload();
},
},
);
})
.catch((err) => {
notifications.push("error", err.message);
});
}, 3000);
if (opts["users"]) {
alert(
"An administrator user has been created for you. Use admin/admin to log in. Don't forget to change the default password.",
);
}
})
.catch((err) => {
notifications.push("error", err.message);
});
},
},
},
"ABRACADABRA!",
),
]);
"ABRACADABRA!",
),
]),
];
},
};
};