alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
264 lines (263 loc) • 8.43 kB
JavaScript
import { $atom, $module, z } from "alepha";
import { $head } from "alepha/react/head";
import { $cookie } from "alepha/server/cookies";
import { useEffect, useState } from "react";
import { useStore } from "alepha/react";
//#region ../../src/react/ui/atoms/uiThemeListAtom.ts
/**
* Available themes the user can pick from. Apps populate this atom on boot
* (e.g. `alepha.store.set(uiThemeListAtom, MY_THEMES)`); UI consumers like
* `<ButtonTheme/>` read it to render a picker. The selected theme id is
* persisted separately in `uiAtom.theme`.
*
* Defaults to a single `"default"` entry so the registry stays usable when
* an app doesn't declare its own list.
*/
const uiThemeListAtom = $atom({
name: "alepha.react.ui.themes",
schema: z.array(z.object({
/** Stable id stored in `uiAtom.theme`. Mapped to a CSS class on `<html>`. */
id: z.string(),
/** Human-readable label shown in the picker. */
label: z.string(),
/**
* Optional 4-color preview swatch in 2×2 order (TL, TR, BL, BR). Any
* CSS-valid color string.
*/
swatch: z.array(z.string()).min(4).max(4).optional(),
/**
* Optional stylesheet URL (typically Google Fonts) loaded lazily when
* the theme is selected.
*/
fontHref: z.string().optional()
})),
default: [{
id: "default",
label: "Default"
}]
});
//#endregion
//#region ../../src/react/ui/atoms/uiAtom.ts
/**
* Persisted UI state — color mode, theme palette, sidebar collapsed state, etc.
*
* The atom is bound to a single `alepha-ui` cookie via {@link UiPersistence},
* so values survive page reloads and are available during SSR.
*/
const uiAtom = $atom({
name: "alepha.react.ui",
schema: z.object({
/** Color mode preference. `"system"` follows the OS-level setting. */
mode: z.enum([
"light",
"dark",
"system"
]),
/** Theme palette name. UI consumers map this to a CSS class on the root. */
theme: z.string(),
/** Sidebar UI state. */
sidebar: z.object({ collapsed: z.boolean() })
}),
default: {
mode: "system",
theme: "default",
sidebar: { collapsed: false }
}
});
//#endregion
//#region ../../src/react/ui/services/UiPersistence.ts
/**
* Inline `<script>` rendered by SSR into the document `<head>`. Runs
* synchronously before any CSS or React hydration: reads the `alepha-ui`
* cookie, resolves `mode === "system"` via `prefers-color-scheme`, and
* applies `class="dark"` (and optional `theme-<name>`) on `<html>`.
*
* This is what kills the flash-of-wrong-theme (FOUC) you'd otherwise get
* with React-effect-based theming. Failures are swallowed silently — at
* worst the page paints in light mode for one frame.
*/
const colorSchemeBoot = `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)alepha-ui=([^;]+)/);var s=m?JSON.parse(decodeURIComponent(m[1])):{};var mode=s.mode||"system";var dark=mode==="dark"||(mode==="system"&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches);var r=document.documentElement;if(dark)r.classList.add("dark");if(s.theme&&s.theme!=="default")r.classList.add("theme-"+s.theme);}catch(e){}})();`;
/**
* Binds the {@link uiAtom} to an `alepha-ui` cookie + injects an inline
* boot script into the SSR head to prevent FOUC on first paint.
*
* Reading flow: on app boot the cookie is parsed and pushed into the atom
* (via the `key` option on `$cookie`). Writing flow: every time the atom
* mutates, the cookie is rewritten — a single `useStore(uiAtom)` call is
* enough to persist UI preferences across reloads.
*
* Persists for 365 days; SameSite=lax so it travels on top-level navigation
* but not on cross-origin requests.
*/
var UiPersistence = class {
ui = $cookie({
name: "alepha-ui",
key: uiAtom.key,
schema: uiAtom.schema,
ttl: [365, "days"],
sameSite: "lax"
});
head = $head({ script: [{ content: colorSchemeBoot }] });
};
//#endregion
//#region ../../src/react/ui/hooks/useColorMode.ts
/**
* Read and update the user's color-mode preference. `"system"` resolves to
* the OS preference and updates live as the OS toggles between light/dark.
*
* @example
* const { mode, setMode, resolved } = useColorMode();
* setMode("dark");
* document.documentElement.classList.toggle("dark", resolved === "dark");
*/
const useColorMode = () => {
const [state, set] = useStore(uiAtom);
const mode = state?.mode ?? "system";
return {
mode,
resolved: useResolvedColorMode(mode),
setMode: (next) => {
set({
...state ?? uiAtom.options.default,
mode: next
});
}
};
};
const useResolvedColorMode = (mode) => {
const [systemDark, setSystemDark] = useState(false);
useEffect(() => {
if (typeof window === "undefined") return;
const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
if (!mq) return;
setSystemDark(mq.matches);
const onChange = (ev) => setSystemDark(ev.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
if (mode === "dark") return "dark";
if (mode === "light") return "light";
return systemDark ? "dark" : "light";
};
//#endregion
//#region ../../src/react/ui/hooks/useTheme.ts
/**
* Read and update the active theme palette name. UI consumers typically map
* the value to a class on the document root (e.g. `theme-claude`).
*
* @example
* const { theme, setTheme } = useTheme();
* setTheme("claude");
*/
const useTheme = () => {
const [state, set] = useStore(uiAtom);
return {
theme: state?.theme ?? "default",
setTheme: (next) => {
set({
...state ?? uiAtom.options.default,
theme: next
});
}
};
};
//#endregion
//#region ../../src/react/ui/components/ColorScheme.tsx
/**
* Applies `class="dark"` and an optional theme palette class
* (`theme-<name>`) to the document root, syncing whenever the underlying
* atom mutates.
*
* Mount once near the root of your tree (typically inside the layout).
*
* @example
* <ColorScheme />
*/
function ColorScheme() {
const { resolved } = useColorMode();
const { theme } = useTheme();
useEffect(() => {
if (typeof document === "undefined") return;
document.documentElement.classList.toggle("dark", resolved === "dark");
}, [resolved]);
useEffect(() => {
if (typeof document === "undefined") return;
const root = document.documentElement;
const previous = [];
root.classList.forEach((cls) => {
if (cls.startsWith("theme-")) previous.push(cls);
});
for (const cls of previous) root.classList.remove(cls);
if (theme && theme !== "default") root.classList.add(`theme-${theme}`);
}, [theme]);
return null;
}
//#endregion
//#region ../../src/react/ui/hooks/useSidebarState.ts
/**
* Read and update the sidebar collapsed state. The value is persisted via the
* `alepha-ui` cookie so it survives reloads and is available during SSR — no
* flash of expanded-then-collapsed when the user prefers a collapsed shell.
*
* @example
* const { collapsed, setCollapsed, toggle } = useSidebarState();
*/
const useSidebarState = () => {
const [state, set] = useStore(uiAtom);
const collapsed = state?.sidebar.collapsed ?? false;
const setCollapsed = (next) => {
const base = state ?? uiAtom.options.default;
set({
...base,
sidebar: {
...base.sidebar,
collapsed: next
}
});
};
return {
collapsed,
setCollapsed,
toggle: () => setCollapsed(!collapsed)
};
};
//#endregion
//#region ../../src/react/ui/services/SchemaControl.ts
/**
* Resolve a raw `$control` value (object or function) into a concrete
* partial config. Returns `null` when the field should be hidden.
*/
const resolveSchemaControl = (raw, context) => {
if (raw == null) return {};
if (typeof raw === "function") {
const result = raw(context);
if (result === false) return null;
if (!result) return {};
if (result.hidden) return null;
return result;
}
if (typeof raw === "object") {
const obj = raw;
if (obj.hidden) return null;
return obj;
}
return {};
};
//#endregion
//#region ../../src/react/ui/index.ts
/**
* Persisted UI state: color mode, theme palette, sidebar collapsed state.
*
* Backed by an `alepha-ui` cookie so preferences survive reloads and are
* available during SSR (no flash of wrong theme).
*
* @module alepha.react.ui
*/
const AlephaReactUi = $module({
name: "alepha.react.ui",
atoms: [uiThemeListAtom],
services: [UiPersistence]
});
//#endregion
export { AlephaReactUi, ColorScheme, UiPersistence, resolveSchemaControl, uiAtom, uiThemeListAtom, useColorMode, useSidebarState, useTheme };
//# sourceMappingURL=index.js.map