This commit is contained in:
Nathan Anderson 2026-02-15 03:00:24 +00:00
parent d487282fa5
commit dc6053fc71
8 changed files with 2597 additions and 73 deletions

View File

@ -121,9 +121,9 @@ Checks and fix:
`_fresh/server.js` and `_fresh/static/` are found (e.g. in systemd set `_fresh/server.js` and `_fresh/static/` are found (e.g. in systemd set
`WorkingDirectory=` to the app directory). `WorkingDirectory=` to the app directory).
3. For reproducible builds across machines, consider re-enabling the lockfile 3. For reproducible builds across machines, consider re-enabling the lockfile in
in `deno.json` (`"lock": true`) and committing `deno.lock`, then using the `deno.json` (`"lock": true`) and committing `deno.lock`, then using the same
same lock on the server. lock on the server.
4. Do not use `esm.sh` for Preact (this project already uses `npm:preact`); 4. Do not use `esm.sh` for Preact (this project already uses `npm:preact`);
duplicate Preact from mixed sources is a common cause. duplicate Preact from mixed sources is a common cause.

View File

@ -1,9 +1,8 @@
{ {
"lock": false,
"tasks": { "tasks": {
"check": "deno fmt && deno fmt --check . && deno lint . && deno check", "check": "deno fmt && deno fmt --check . && deno lint . && deno check",
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "deno install -r --allow-scripts && vite build",
"start": "deno serve -A --port 8080 _fresh/server.js", "start": "deno serve -A --port 8080 _fresh/server.js",
"update": "deno run -A -r jsr:@fresh/update .", "update": "deno run -A -r jsr:@fresh/update .",
"e2e": "deno run -A npm:playwright test", "e2e": "deno run -A npm:playwright test",
@ -75,12 +74,8 @@
], ],
"types": ["vite/client"] "types": ["vite/client"]
}, },
"test": { "test": { "exclude": ["node_modules/", "_fresh/"] },
"exclude": ["node_modules/", "_fresh/"] "fmt": { "exclude": ["node_modules/", "_fresh/"] },
},
"fmt": {
"exclude": ["node_modules/", "_fresh/"]
},
"nodeModulesDir": "auto", "nodeModulesDir": "auto",
"unstable": ["kv"] "unstable": ["kv"]
} }

2502
deno.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -11,7 +11,8 @@ interface MemberHomeProps {
} }
export default function MemberHome( export default function MemberHome(
{ onboardingStatus, serverCalendarYear, serverCalendarMonth }: MemberHomeProps, { onboardingStatus, serverCalendarYear, serverCalendarMonth }:
MemberHomeProps,
) { ) {
useEffect(() => { useEffect(() => {
const lockRoute = localStorage.getItem("lock-route"); const lockRoute = localStorage.getItem("lock-route");

View File

@ -0,0 +1,71 @@
import { useEffect } from "preact/hooks";
const THEME_KEY = "cyberhybrid-theme-preference";
function applyTheme(root: Element, mode: string): void {
root.setAttribute("data-theme-mode", mode);
}
function readTheme(): string {
try {
const stored = localStorage.getItem(THEME_KEY);
if (stored === "light" || stored === "dark") return stored;
} catch {
// ignore
}
return "dark";
}
function syncToggleLabels(root: Element): void {
const isDark = root.getAttribute("data-theme-mode") !== "light";
const label = isDark ? "Switch to light mode" : "Switch to dark mode";
const toggles = root.querySelectorAll("[data-theme-toggle]");
toggles.forEach((el) => {
el.setAttribute("aria-label", label);
el.setAttribute("title", label);
});
}
function persistTheme(mode: string): void {
try {
localStorage.setItem(THEME_KEY, mode);
} catch {
// ignore
}
}
/**
* Client-only island that applies saved theme and wires up theme toggle clicks.
* Renders nothing; runs theme logic in useEffect to avoid dangerouslySetInnerHTML.
*/
export default function ThemeHydration() {
useEffect(() => {
const root = document.documentElement;
applyTheme(root, readTheme());
syncToggleLabels(root);
const handleClick = (event: Event) => {
const target = event.target as Node;
if (!target || !(target instanceof Element)) return;
const toggle = target.closest("[data-theme-toggle]");
if (!toggle) return;
event.preventDefault();
event.stopPropagation();
const current = root.getAttribute("data-theme-mode") === "light"
? "light"
: "dark";
const next = current === "dark" ? "light" : "dark";
applyTheme(root, next);
persistTheme(next);
syncToggleLabels(root);
};
document.addEventListener("click", handleClick, true);
return () => document.removeEventListener("click", handleClick, true);
}, []);
return null;
}

View File

@ -13,7 +13,8 @@ import {
* Native-rendered dropdown content for <details> menu. * Native-rendered dropdown content for <details> menu.
*/ */
export default function NavigationMenu( export default function NavigationMenu(
{ isLoggedIn, currentRoute, navigationGroups, serverCalendarPath }: NavigationMenuProps, { isLoggedIn, currentRoute, navigationGroups, serverCalendarPath }:
NavigationMenuProps,
) { ) {
function getItemStateClass(item: NavigationItem): string { function getItemStateClass(item: NavigationItem): string {
if (item.disabled) return "navigation-menu-item--disabled"; if (item.disabled) return "navigation-menu-item--disabled";

View File

@ -16,7 +16,14 @@ function normalizeRoute(path: string): string {
* Main navigation bar with user info, encryption status, and menu. * Main navigation bar with user info, encryption status, and menu.
*/ */
export default function WelcomeBar( export default function WelcomeBar(
{ publicId, user_name, currentRoute, isEncrypted, stateId, serverCalendarPath }: WelcomeBarProps, {
publicId,
user_name,
currentRoute,
isEncrypted,
stateId,
serverCalendarPath,
}: WelcomeBarProps,
) { ) {
const [activeRoute, setActiveRoute] = useState(normalizeRoute(currentRoute)); const [activeRoute, setActiveRoute] = useState(normalizeRoute(currentRoute));
const [state] = useState<WelcomeBarState>({ const [state] = useState<WelcomeBarState>({

View File

@ -1,6 +1,7 @@
// _app.tsx // _app.tsx
import { define, type PageProps } from "../utils.ts"; import { define, type PageProps } from "../utils.ts";
import WelcomeBar from "../islands/WelcomeBar/WelcomeBar.tsx"; import WelcomeBar from "../islands/WelcomeBar/WelcomeBar.tsx";
import ThemeHydration from "../islands/ThemeHydration.tsx";
import { createPublicId } from "../helpers.ts"; import { createPublicId } from "../helpers.ts";
import { SessionCookie } from "./auth/callback.ts"; import { SessionCookie } from "./auth/callback.ts";
@ -24,10 +25,12 @@ export default define.page(async function App(props: PageProps) {
: ""; : "";
const now = new Date(); const now = new Date();
const serverCalendarPath = const serverCalendarPath =
`/calendar/financial-calendar/${now.getFullYear()}/${(now.getMonth() + 1).toString().padStart(2, "0")}`; `/calendar/financial-calendar/${now.getFullYear()}/${
(now.getMonth() + 1).toString().padStart(2, "0")
}`;
return ( return (
<html> <html data-theme-mode="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@ -45,63 +48,7 @@ export default define.page(async function App(props: PageProps) {
<div class="page-offset"> <div class="page-offset">
<Component /> <Component />
</div> </div>
<script <ThemeHydration />
dangerouslySetInnerHTML={{
__html: `(function () {
var KEY = "cyberhybrid-theme-preference";
var root = document.documentElement;
function applyTheme(mode) {
root.setAttribute("data-theme-mode", mode);
}
function readTheme() {
try {
var stored = localStorage.getItem(KEY);
if (stored === "light" || stored === "dark") return stored;
} catch (_error) {}
return "dark";
}
function syncToggleLabels() {
var isDark = root.getAttribute("data-theme-mode") !== "light";
var label = isDark ? "Switch to light mode" : "Switch to dark mode";
var toggles = document.querySelectorAll("[data-theme-toggle]");
for (var i = 0; i < toggles.length; i++) {
toggles[i].setAttribute("aria-label", label);
toggles[i].setAttribute("title", label);
}
}
function persistTheme(mode) {
try {
localStorage.setItem(KEY, mode);
} catch (_error) {}
}
applyTheme(readTheme());
syncToggleLabels();
document.addEventListener("click", function (event) {
var target = event.target;
if (!(target instanceof Element)) return;
var toggle = target.closest("[data-theme-toggle]");
if (!toggle) return;
event.preventDefault();
event.stopPropagation();
var current = root.getAttribute("data-theme-mode") === "light"
? "light"
: "dark";
var next = current === "dark" ? "light" : "dark";
applyTheme(next);
persistTheme(next);
syncToggleLabels();
}, true);
})();`,
}}
/>
</body> </body>
</html> </html>
); );