feat(themes): Omarchy catalog, 12 light themes, Dark/Light Appearance picker
Expand the catalog from 11 themes (10 dark, 1 light) to 37 (24/13): all 18 remaining Omarchy defaults (palettes from each theme's colors.toml) plus 8 canonical light themes (Tokyo Night Day, Solarized Light, One Light, Ayu Light, GitHub Light, Gruvbox Light, Everforest Light, Kanagawa Lotus). Lupine leads the light list as the default light theme; the app default stays tokyo-night. Appearance panel: Dark/Light segmented filter (SSR-initialized from the user's current theme), scrollable theme list, Bookshelf section pinned; active row revealed on open. Light themes carry color-scheme: light and the soft-shadow token set. Public-page theme selects get Dark/Light optgroups. New moon/sun icons. tailwind.config: safelist the new theme ids — Tailwind v3 purges custom @layer base class selectors that appear in no scanned content file, so an unsafelisted id silently ships without its CSS (found the hard way). theme.ts: applyTheme now swaps only theme-* classes (changing theme from the login select no longer wipes min-h-screen) and keeps the class in sync on <html> for the new ThemeFlashGuard — a pre-paint inline script on the four public pages that applies the saved theme before first paint. Kanagawa's UI accent uses crystalBlue (#7e9cd8) instead of Omarchy's foreground-as-accent so links stay distinguishable from text.
This commit is contained in:
1 parent
4f913da0b1
commit
d5bb874651
21 files changed
+1120
-289
No files matched your search
+57
-4
@@ -12,16 +12,53 @@ type ThemeType =
|
||||
| "catppuccin-mocha"
|
||||
| "catppuccin-macchiato"
|
||||
| "catppuccin-frappe"
|
||||
| "catppuccin-latte";
|
||||
// Omarchy dark themes
|
||||
| "vantablack"
|
||||
| "last-horizon"
|
||||
| "ethereal"
|
||||
| "hackerman"
|
||||
| "matte-black"
|
||||
| "solitude"
|
||||
| "retro-82"
|
||||
| "osaka-jade"
|
||||
| "kanagawa"
|
||||
| "miasma"
|
||||
| "lumon"
|
||||
| "ristretto"
|
||||
| "gruvbox"
|
||||
| "everforest"
|
||||
// Light themes
|
||||
| "tokyo-night-day"
|
||||
| "solarized-light"
|
||||
| "one-light"
|
||||
| "ayu-light"
|
||||
| "github-light"
|
||||
| "gruvbox-light"
|
||||
| "everforest-light"
|
||||
| "kanagawa-lotus"
|
||||
| "catppuccin-latte"
|
||||
| "rose-pine"
|
||||
| "lupine"
|
||||
| "flexoki-light"
|
||||
| "white";
|
||||
|
||||
const DEFAULT_THEME: ThemeType = "tokyo-night";
|
||||
const THEME_STORAGE_KEY = "theme";
|
||||
const TOKEN_STORAGE_KEY = "token";
|
||||
|
||||
// Apply theme to document body
|
||||
// Apply theme to document body. Replaces only `theme-*` classes and keeps
|
||||
// the class in sync on <html> as well (ThemeFlashGuard may have put one
|
||||
// there on public pages), so unrelated classes like `min-h-screen` on the
|
||||
// auth pages survive a theme change.
|
||||
const applyTheme = (theme: string): void => {
|
||||
// Apply regular theme only
|
||||
document.body.className = `theme-${theme}`;
|
||||
const setThemeClass = (el: Element): void => {
|
||||
[...el.classList].forEach((c) => {
|
||||
if (c.startsWith("theme-")) el.classList.remove(c);
|
||||
});
|
||||
el.classList.add(`theme-${theme}`);
|
||||
};
|
||||
setThemeClass(document.documentElement);
|
||||
setThemeClass(document.body);
|
||||
document.body.style.background = "";
|
||||
document.body.style.backgroundSize = "";
|
||||
document.body.style.backgroundAttachment = "";
|
||||
@@ -102,6 +139,20 @@ const updateThemeIndicators = (): void => {
|
||||
});
|
||||
};
|
||||
|
||||
// Scroll the active theme's row into view inside the Appearance panel's
|
||||
// theme list, for the given mode. No-op when the active theme lives in the
|
||||
// other mode (its row is filtered out there).
|
||||
const revealCurrentTheme = (mode: string): void => {
|
||||
const match = document.body.className.match(/theme-([\w-]+)/);
|
||||
const current = match ? match[1] : DEFAULT_THEME;
|
||||
const btn = document.querySelector<HTMLElement>(
|
||||
`.theme-btn[data-theme="${current}"]`,
|
||||
);
|
||||
if (btn && btn.dataset.mode === mode) {
|
||||
btn.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize theme system
|
||||
const initializeTheme = (): void => {
|
||||
loadTheme();
|
||||
@@ -228,6 +279,7 @@ Alpine.data("theme", () => ({
|
||||
loadTheme,
|
||||
loadUserTheme,
|
||||
loadWoodPaneling,
|
||||
revealCurrentTheme,
|
||||
updateThemeIndicators,
|
||||
updateWoodPanelingIndicators,
|
||||
}));
|
||||
@@ -240,6 +292,7 @@ export {
|
||||
loadTheme,
|
||||
loadUserTheme,
|
||||
loadWoodPaneling,
|
||||
revealCurrentTheme,
|
||||
updateThemeIndicators,
|
||||
updateWoodPanelingIndicators,
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user