From 4a191afdec5cced9b1e0b4501737797dbf5fa780 Mon Sep 17 00:00:00 2001 From: Zaid Humaimidi Date: Mon, 9 Mar 2026 00:00:07 -0700 Subject: [PATCH] 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 --- build/build.ts | 38 +- build/generate-fonts.sh | 18 + lib/common/expression/pagination.ts | 8 +- lib/ui.ts | 15 +- npm-shrinkwrap.json | 1992 +++++++++++++++++++++++++-- package.json | 11 +- tsconfig.json | 4 +- ui/admin-menu.ts | 88 -- ui/app.ts | 70 +- ui/autocomplete-compnent.ts | 10 +- ui/change-password-component.ts | 60 +- ui/code-editor-component.ts | 75 +- ui/codemirror-loader.ts | 9 + ui/components.ts | 2 +- ui/components/all-parameters.ts | 87 +- ui/components/device-actions.ts | 10 +- ui/components/device-faults.ts | 131 +- ui/components/device-link.ts | 2 +- ui/components/overview-dot.ts | 11 +- ui/components/parameter-list.ts | 15 +- ui/components/parameter-table.ts | 65 +- ui/components/parameter.ts | 12 +- ui/components/ping.ts | 2 +- ui/components/summon-button.ts | 4 +- ui/components/tags.ts | 49 +- ui/config-page.ts | 67 +- ui/css/InterVariable-Italic.woff2 | Bin 0 -> 75112 bytes ui/css/InterVariable.woff2 | Bin 0 -> 68360 bytes ui/css/RobotoMono.woff2 | Bin 0 -> 33228 bytes ui/css/app.css | 919 ++---------- ui/css/normalize.css | 450 ------ ui/device-page.ts | 10 +- ui/devices-page.ts | 16 +- ui/drawer-component.ts | 357 +++-- ui/dynamic-loader.ts | 19 +- ui/error-page.ts | 2 +- ui/faults-page.ts | 20 +- ui/files-page.ts | 18 +- ui/filter-component.ts | 59 +- ui/icons.ts | 10 - ui/icons/close.svg | 1 + ui/icons/menu.svg | 1 + ui/index-table-component.ts | 235 +++- ui/layout.ts | 61 - ui/layout.tsx | 317 +++++ ui/login-page.ts | 96 -- ui/login-page.tsx | 188 +++ ui/long-text-component.ts | 38 +- ui/menu.ts | 63 - ui/overlay.ts | 53 +- ui/overview-page.ts | 27 +- ui/permissions-page.ts | 33 +- ui/pie-chart-component.ts | 60 +- ui/presets-page.ts | 44 +- ui/provisions-page.ts | 14 +- ui/put-form-component.ts | 129 +- ui/store.ts | 8 +- ui/tailwind-utility-components.ts | 200 +++ ui/ui-config-component.ts | 15 +- ui/user-menu.ts | 50 - ui/users-page.ts | 13 +- ui/virtual-parameters-page.ts | 17 +- ui/wizard-page.ts | 169 +-- ui/yaml-loader.ts | 1 + 64 files changed, 4147 insertions(+), 2421 deletions(-) create mode 100755 build/generate-fonts.sh delete mode 100644 ui/admin-menu.ts create mode 100644 ui/codemirror-loader.ts create mode 100644 ui/css/InterVariable-Italic.woff2 create mode 100644 ui/css/InterVariable.woff2 create mode 100644 ui/css/RobotoMono.woff2 delete mode 100644 ui/css/normalize.css delete mode 100644 ui/icons.ts create mode 100644 ui/icons/close.svg create mode 100644 ui/icons/menu.svg delete mode 100644 ui/layout.ts create mode 100644 ui/layout.tsx delete mode 100644 ui/login-page.ts create mode 100644 ui/login-page.tsx delete mode 100644 ui/menu.ts create mode 100644 ui/tailwind-utility-components.ts delete mode 100644 ui/user-menu.ts create mode 100644 ui/yaml-loader.ts diff --git a/build/build.ts b/build/build.ts index 1ae3f65..d07b70e 100644 --- a/build/build.ts +++ b/build/build.ts @@ -110,12 +110,28 @@ const inlineDepsPlugin = { const deps = [ "parsimmon", "espresso-iisojs", - "codemirror", + "@codemirror", "mithril", "yaml", ]; + const depFiles = new Set(); build.onResolve({ filter: /^[^.]/ }, async (args) => { - if (deps.some((d) => args.path.startsWith(d))) return undefined; + if (args.pluginData === "inlineDeps") return undefined; + if ( + depFiles.has(args.importer) || + deps.some((d) => args.path.startsWith(d)) + ) { + const res = await build.resolve(args.path, { + importer: args.importer, + namespace: args.namespace, + resolveDir: args.resolveDir, + kind: args.kind, + with: args.with, + pluginData: "inlineDeps", + }); + depFiles.add(res.path); + return res; + } return { sideEffects: false, external: true }; }); }, @@ -226,6 +242,16 @@ async function copyStatic(): Promise { } async function generateCss(): Promise { + const tailwindPlugin = { + name: "tailwind", + setup(build) { + build.onLoad({ filter: /\/ui\/css\/app.css$/ }, async (args) => { + const res = await execAsync(`npx @tailwindcss/cli -i ${args.path}`); + return { loader: "css", contents: res.stdout }; + }); + }, + } as esbuild.Plugin; + const res = await esbuild.build({ bundle: true, absWorkingDir: INPUT_DIR, @@ -235,7 +261,11 @@ async function generateCss(): Promise { entryPoints: ["ui/css/app.css"], entryNames: "[dir]/[name]-[hash]", outfile: path.join(OUTPUT_DIR, "public/app.css"), - target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"], + plugins: [tailwindPlugin], + loader: { + ".woff2": "dataurl", + }, + target: ["chrome111", "safari16.4", "firefox128"], metafile: true, }); @@ -293,7 +323,7 @@ async function generateFrontendJs(): Promise { sourcesContent: false, platform: "browser", format: "esm", - target: ["chrome109", "safari15.6", "firefox115", "opera102", "edge118"], + target: ["chrome111", "safari16.4", "firefox128"], entryPoints: ["ui/app.ts"], entryNames: "[dir]/[name]-[hash]", outdir: path.join(OUTPUT_DIR, "public"), diff --git a/build/generate-fonts.sh b/build/generate-fonts.sh new file mode 100755 index 0000000..81256ca --- /dev/null +++ b/build/generate-fonts.sh @@ -0,0 +1,18 @@ +#!/bin/bash + +cd "$(dirname "$0")" + +declare -A FONTS=( + [InterVariable]=https://rsms.me/inter/font-files/InterVariable.woff2 + [InterVariable-Italic]=https://rsms.me/inter/font-files/InterVariable-Italic.woff2 + [RobotoMono]=https://raw.githubusercontent.com/googlefonts/RobotoMono/main/fonts/variable/RobotoMono%5Bwght%5D.ttf +) + +for name in "${!FONTS[@]}" +do + url="${FONTS[$name]}" + echo "Downloading $url" + TMP=$(mktemp) + curl "$url" -s --output "$TMP" + pyftsubset "$TMP" --output-file="../ui/css/$name.woff2" --flavor=woff2 --unicodes="U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD" --layout-features+="tnum" +done diff --git a/lib/common/expression/pagination.ts b/lib/common/expression/pagination.ts index 462fac8..f232b64 100644 --- a/lib/common/expression/pagination.ts +++ b/lib/common/expression/pagination.ts @@ -1,8 +1,8 @@ import { complement } from "espresso-iisojs"; -import { Expression } from "../../types"; -import normalize from "./normalize"; -import { Clause, SynthContext } from "./synth"; -import { and, or } from "./util"; +import { Expression } from "../../types.ts"; +import normalize from "./normalize.ts"; +import { Clause, SynthContext } from "./synth.ts"; +import { and, or } from "./util.ts"; type Bookmark = Record; diff --git a/lib/ui.ts b/lib/ui.ts index 502fdc9..8a1c444 100644 --- a/lib/ui.ts +++ b/lib/ui.ts @@ -105,6 +105,9 @@ router.post("/login", async (ctx) => { const username = ctx.request.body.username; const password = ctx.request.body.password; + const remember = ctx.request.body.remember; + const TWO_WEEKS_SECS = 1209600; + const ONE_DAY_SECS = 86400; const log = { message: "Log in", @@ -115,8 +118,14 @@ router.post("/login", async (ctx) => { function success(authMethod): void { log.method = authMethod; - const token = jwt.sign({ username, authMethod }, JWT_SECRET); - ctx.cookies.set(JWT_COOKIE, token, { sameSite: "lax" }); + const expiresIn = remember ? TWO_WEEKS_SECS : ONE_DAY_SECS; + const token = jwt.sign({ username, authMethod }, JWT_SECRET, { + expiresIn, + }); + ctx.cookies.set(JWT_COOKIE, token, { + sameSite: "lax", + maxAge: remember ? expiresIn * 1000 : null, + }); ctx.body = JSON.stringify(token); logger.accessInfo(log); } @@ -219,7 +228,7 @@ router.get("/", async (ctx) => { - +