Theme menu: the same palettes as INBUXA webmail
Under the user menu, Theme offers ihasmail's palettes, in the same order and under the same names. ihasmail is the default look, the colors the admin already had, until someone picks another. The choice is remembered and applied before the first paint, and dark mode still toggles on its own. The other palettes' colors are ihasmail's, already contrast-checked, mapped onto the admin's tokens by scripts/import-palettes.py; re-running it re-syncs them. NOTICE carries the palettes' MIT notices and the fonts' OFL notices.
This commit is contained in:
@@ -8,7 +8,7 @@
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import * as LucideIcons from 'lucide-react';
|
||||
const { Sun, Moon, User, LogOut, Check, Menu, Search, FileCode } = LucideIcons;
|
||||
const { Sun, Moon, User, LogOut, Check, Menu, Search, FileCode, Palette } = LucideIcons;
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CommandPalette } from '@/components/common/CommandPalette';
|
||||
import {
|
||||
@@ -17,9 +17,15 @@ import {
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuRadioGroup,
|
||||
DropdownMenuRadioItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import { isPaletteId, PALETTES } from '@/lib/palettes';
|
||||
import Logo from '@/components/common/Logo';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { EnterpriseUpsell } from '@/components/common/EnterpriseUpsell';
|
||||
@@ -49,6 +55,8 @@ export function TopBar() {
|
||||
const navigate = useNavigate();
|
||||
const theme = useUIStore((s) => s.theme);
|
||||
const toggleTheme = useUIStore((s) => s.toggleTheme);
|
||||
const palette = useUIStore((s) => s.palette);
|
||||
const setPalette = useUIStore((s) => s.setPalette);
|
||||
const toggleSidebar = useUIStore((s) => s.toggleSidebar);
|
||||
const setActiveSection = useUIStore((s) => s.setActiveSection);
|
||||
const activeSection = useUIStore((s) => s.activeSection);
|
||||
@@ -216,6 +224,31 @@ export function TopBar() {
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* INBUXA: the same palettes as INBUXA webmail. */}
|
||||
<DropdownMenuSub>
|
||||
<DropdownMenuSubTrigger>
|
||||
<Palette className="mr-2 h-4 w-4" />
|
||||
{t('theme.menu', 'Theme')}
|
||||
</DropdownMenuSubTrigger>
|
||||
<DropdownMenuSubContent className="w-52">
|
||||
<DropdownMenuRadioGroup value={palette} onValueChange={(v) => isPaletteId(v) && setPalette(v)}>
|
||||
{PALETTES.map((p) => (
|
||||
<DropdownMenuRadioItem key={p.id} value={p.id} className="gap-2">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-3.5 w-3.5 shrink-0 rounded-full ring-1 ring-black/10 dark:ring-white/15"
|
||||
style={{ background: theme === 'dark' ? p.swatch[1] : p.swatch[0] }}
|
||||
/>
|
||||
<span className="notranslate" translate="no">
|
||||
{p.id === 'default' ? t('theme.classic', 'Classic') : p.name}
|
||||
</span>
|
||||
</DropdownMenuRadioItem>
|
||||
))}
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuSubContent>
|
||||
</DropdownMenuSub>
|
||||
<DropdownMenuSeparator />
|
||||
|
||||
<DropdownMenuItem asChild>
|
||||
<a href={sourceDownloadUrl()} target="_blank" rel="noopener noreferrer">
|
||||
<FileCode className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -391,5 +391,9 @@
|
||||
"evening": "Good evening",
|
||||
"subtitle": "Here's how your mail server is doing.",
|
||||
"signedInAs": "Signed in as {{username}}"
|
||||
},
|
||||
"theme": {
|
||||
"menu": "Theme",
|
||||
"classic": "Classic"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
@import 'tailwindcss';
|
||||
@import './palettes.css';
|
||||
|
||||
:root {
|
||||
/* INBUXA, light: warm paper, the mark's navy for text, its teal to act. */
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/**
|
||||
* The themes the admin offers: the same palettes as INBUXA webmail, in the
|
||||
* same order and under the same names. "ihasmail" is the default, the look in
|
||||
* index.css. The others' colors are in palettes.css, generated from ihasmail's
|
||||
* by scripts/import-palettes.py.
|
||||
*/
|
||||
export type PaletteId =
|
||||
| 'default'
|
||||
| 'ihasmail'
|
||||
| 'dracula'
|
||||
| 'gruvbox'
|
||||
| 'rose-pine'
|
||||
| 'tokyo-night'
|
||||
| 'catppuccin'
|
||||
| 'solarized'
|
||||
| 'ayu'
|
||||
| 'kanagawa'
|
||||
| 'everforest'
|
||||
| 'primer';
|
||||
|
||||
export const DEFAULT_PALETTE: PaletteId = 'ihasmail';
|
||||
|
||||
/** `swatch` is the palette's accent, light and dark, for the menu's color dot. */
|
||||
export const PALETTES: { id: PaletteId; name: string; swatch: [string, string] }[] = [
|
||||
{ id: 'default', name: 'Classic', swatch: ['#0f766e', '#2dd4bf'] },
|
||||
{ id: 'ihasmail', name: 'ihasmail', swatch: ['#0d8a82', '#46cac3'] },
|
||||
{ id: 'dracula', name: 'Dracula', swatch: ['#644ac9', '#bd93f9'] },
|
||||
{ id: 'gruvbox', name: 'Gruvbox', swatch: ['#076678', '#83a598'] },
|
||||
{ id: 'rose-pine', name: 'Rosé Pine', swatch: ['#907aa9', '#c4a7e7'] },
|
||||
{ id: 'tokyo-night', name: 'Tokyo Night', swatch: ['#2959aa', '#7aa2f7'] },
|
||||
{ id: 'catppuccin', name: 'Catppuccin', swatch: ['#8839ef', '#cba6f7'] },
|
||||
{ id: 'solarized', name: 'Solarized', swatch: ['#268bd2', '#268bd2'] },
|
||||
{ id: 'ayu', name: 'Ayu', swatch: ['#cb7f14', '#e6b450'] },
|
||||
{ id: 'kanagawa', name: 'Kanagawa', swatch: ['#624c83', '#7e9cd8'] },
|
||||
{ id: 'everforest', name: 'Everforest', swatch: ['#7d8f01', '#a7c080'] },
|
||||
{ id: 'primer', name: 'Primer', swatch: ['#0969da', '#58a6ff'] },
|
||||
];
|
||||
|
||||
export function isPaletteId(v: unknown): v is PaletteId {
|
||||
return PALETTES.some((p) => p.id === v);
|
||||
}
|
||||
|
||||
export function applyPalette(id: PaletteId) {
|
||||
if (id === DEFAULT_PALETTE) delete document.documentElement.dataset.palette;
|
||||
else document.documentElement.dataset.palette = id;
|
||||
}
|
||||
@@ -27,6 +27,11 @@ import { loadLogoOnce } from './lib/logoCache';
|
||||
if (persisted) {
|
||||
const parsed = JSON.parse(persisted);
|
||||
const theme = parsed?.state?.theme;
|
||||
// INBUXA: the palette too, before the first paint, so it doesn't flash.
|
||||
const palette = parsed?.state?.palette;
|
||||
if (typeof palette === 'string' && palette !== 'ihasmail' && /^[a-z-]+$/.test(palette)) {
|
||||
document.documentElement.dataset.palette = palette;
|
||||
}
|
||||
if (theme === 'dark' || theme === 'light') {
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark');
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,672 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2026 Coffey Labs
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
/*
|
||||
* Generated by scripts/import-palettes.py from ihasmail's palettes. Don't
|
||||
* edit by hand. The palettes' own credits are in NOTICE.
|
||||
*
|
||||
* No data-palette attribute is the default, "ihasmail": the tokens in index.css.
|
||||
*/
|
||||
|
||||
:root[data-palette="default"] {
|
||||
--background: #f6f8fa;
|
||||
--foreground: #111827;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #111827;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #111827;
|
||||
--content-background: #eef1f4;
|
||||
--primary: #0f766e;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eef1f4;
|
||||
--secondary-foreground: #111827;
|
||||
--muted: #eef1f4;
|
||||
--muted-foreground: #5b6472;
|
||||
--accent: #d9f1ee;
|
||||
--accent-foreground: #0b5750;
|
||||
--highlight: #b45309;
|
||||
--highlight-soft: #fef3c7;
|
||||
--destructive: #dc2626;
|
||||
--border: #e3e7ec;
|
||||
--input: #cbd2da;
|
||||
--ring: #0f766e;
|
||||
--chart-1: #0f766e;
|
||||
--chart-2: #b45309;
|
||||
--chart-3: #2563eb;
|
||||
--chart-4: #16a34a;
|
||||
--chart-5: #9333ea;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="default"] {
|
||||
--background: #0b1220;
|
||||
--foreground: #e5e9f0;
|
||||
--card: #111a2b;
|
||||
--card-foreground: #e5e9f0;
|
||||
--popover: #111a2b;
|
||||
--popover-foreground: #e5e9f0;
|
||||
--content-background: #0d1524;
|
||||
--primary: #2dd4bf;
|
||||
--primary-foreground: #052e2b;
|
||||
--secondary: #0d1524;
|
||||
--secondary-foreground: #e5e9f0;
|
||||
--muted: #0d1524;
|
||||
--muted-foreground: #9aa5b8;
|
||||
--accent: rgba(45, 212, 191, 0.16);
|
||||
--accent-foreground: #99f6e4;
|
||||
--highlight: #fbbf24;
|
||||
--highlight-soft: rgba(251, 191, 36, 0.15);
|
||||
--destructive: #f87171;
|
||||
--border: #1f2a3d;
|
||||
--input: #31405a;
|
||||
--ring: #2dd4bf;
|
||||
--chart-1: #2dd4bf;
|
||||
--chart-2: #fbbf24;
|
||||
--chart-3: #60a5fa;
|
||||
--chart-4: #4ade80;
|
||||
--chart-5: #c084fc;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="dracula"] {
|
||||
--background: #fffbeb;
|
||||
--foreground: #1f1f1f;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1f1f1f;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1f1f1f;
|
||||
--content-background: #f6f1de;
|
||||
--primary: #644ac9;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f6f1de;
|
||||
--secondary-foreground: #1f1f1f;
|
||||
--muted: #f6f1de;
|
||||
--muted-foreground: #6c664b;
|
||||
--accent: #e9e2e6;
|
||||
--accent-foreground: #644ac9;
|
||||
--highlight: #a34d14;
|
||||
--highlight-soft: rgba(163, 77, 20, 0.15);
|
||||
--destructive: #cb3a2a;
|
||||
--border: #cfcfde;
|
||||
--input: #91919a;
|
||||
--ring: #644ac9;
|
||||
--chart-1: #644ac9;
|
||||
--chart-2: #a34d14;
|
||||
--chart-3: #036a96;
|
||||
--chart-4: #14710a;
|
||||
--chart-5: #a3144d;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="dracula"] {
|
||||
--background: #282a36;
|
||||
--foreground: #f8f8f2;
|
||||
--card: #2f3140;
|
||||
--card-foreground: #f8f8f2;
|
||||
--popover: #2f3140;
|
||||
--popover-foreground: #f8f8f2;
|
||||
--content-background: #21222c;
|
||||
--primary: #bd93f9;
|
||||
--primary-foreground: #282a36;
|
||||
--secondary: #21222c;
|
||||
--secondary-foreground: #f8f8f2;
|
||||
--muted: #21222c;
|
||||
--muted-foreground: #8390b7;
|
||||
--accent: rgba(189, 147, 249, 0.16);
|
||||
--accent-foreground: #bf96f9;
|
||||
--highlight: #ffb86c;
|
||||
--highlight-soft: rgba(255, 184, 108, 0.15);
|
||||
--destructive: #ff5555;
|
||||
--border: #44475a;
|
||||
--input: #717381;
|
||||
--ring: #bd93f9;
|
||||
--chart-1: #bd93f9;
|
||||
--chart-2: #ffb86c;
|
||||
--chart-3: #8be9fd;
|
||||
--chart-4: #50fa7b;
|
||||
--chart-5: #ff79c6;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="gruvbox"] {
|
||||
--background: #fbf1c7;
|
||||
--foreground: #3c3836;
|
||||
--card: #f9f5d7;
|
||||
--card-foreground: #3c3836;
|
||||
--popover: #f9f5d7;
|
||||
--popover-foreground: #3c3836;
|
||||
--content-background: #f2e5bc;
|
||||
--primary: #076678;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f2e5bc;
|
||||
--secondary-foreground: #3c3836;
|
||||
--muted: #f2e5bc;
|
||||
--muted-foreground: #776b60;
|
||||
--accent: #d9debc;
|
||||
--accent-foreground: #076678;
|
||||
--highlight: #af3a03;
|
||||
--highlight-soft: rgba(175, 58, 3, 0.15);
|
||||
--destructive: #9d0006;
|
||||
--border: #d5c4a1;
|
||||
--input: #948871;
|
||||
--ring: #076678;
|
||||
--chart-1: #076678;
|
||||
--chart-2: #af3a03;
|
||||
--chart-3: #076678;
|
||||
--chart-4: #746f0d;
|
||||
--chart-5: #8f3f71;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="gruvbox"] {
|
||||
--background: #282828;
|
||||
--foreground: #ebdbb2;
|
||||
--card: #32302f;
|
||||
--card-foreground: #ebdbb2;
|
||||
--popover: #32302f;
|
||||
--popover-foreground: #ebdbb2;
|
||||
--content-background: #1d2021;
|
||||
--primary: #83a598;
|
||||
--primary-foreground: #282828;
|
||||
--secondary: #1d2021;
|
||||
--secondary-foreground: #ebdbb2;
|
||||
--muted: #1d2021;
|
||||
--muted-foreground: #a89984;
|
||||
--accent: rgba(131, 165, 152, 0.16);
|
||||
--accent-foreground: #8cab9f;
|
||||
--highlight: #fe8019;
|
||||
--highlight-soft: rgba(254, 128, 25, 0.15);
|
||||
--destructive: #fb5440;
|
||||
--border: #504945;
|
||||
--input: #787168;
|
||||
--ring: #83a598;
|
||||
--chart-1: #83a598;
|
||||
--chart-2: #fe8019;
|
||||
--chart-3: #83a598;
|
||||
--chart-4: #b8bb26;
|
||||
--chart-5: #d3869b;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="rose-pine"] {
|
||||
--background: #faf4ed;
|
||||
--foreground: #464261;
|
||||
--card: #fffaf3;
|
||||
--card-foreground: #464261;
|
||||
--popover: #fffaf3;
|
||||
--popover-foreground: #464261;
|
||||
--content-background: #f2e9e1;
|
||||
--primary: #907aa9;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #f2e9e1;
|
||||
--secondary-foreground: #464261;
|
||||
--muted: #f2e9e1;
|
||||
--muted-foreground: #716d89;
|
||||
--accent: #ebe3e3;
|
||||
--accent-foreground: #705f84;
|
||||
--highlight: #986622;
|
||||
--highlight-soft: rgba(152, 102, 34, 0.15);
|
||||
--destructive: #a45a6f;
|
||||
--border: #dfd9d2;
|
||||
--input: #908c8b;
|
||||
--ring: #907aa9;
|
||||
--chart-1: #907aa9;
|
||||
--chart-2: #986622;
|
||||
--chart-3: #286983;
|
||||
--chart-4: #467881;
|
||||
--chart-5: #7a6890;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="rose-pine"] {
|
||||
--background: #191724;
|
||||
--foreground: #e0def4;
|
||||
--card: #1f1d2e;
|
||||
--card-foreground: #e0def4;
|
||||
--popover: #1f1d2e;
|
||||
--popover-foreground: #e0def4;
|
||||
--content-background: #14121f;
|
||||
--primary: #c4a7e7;
|
||||
--primary-foreground: #191724;
|
||||
--secondary: #14121f;
|
||||
--secondary-foreground: #e0def4;
|
||||
--muted: #14121f;
|
||||
--muted-foreground: #908caa;
|
||||
--accent: rgba(196, 167, 231, 0.16);
|
||||
--accent-foreground: #c4a7e7;
|
||||
--highlight: #f6c177;
|
||||
--highlight-soft: rgba(246, 193, 119, 0.15);
|
||||
--destructive: #eb6f92;
|
||||
--border: #26233a;
|
||||
--input: #666376;
|
||||
--ring: #c4a7e7;
|
||||
--chart-1: #c4a7e7;
|
||||
--chart-2: #f6c177;
|
||||
--chart-3: #9ccfd8;
|
||||
--chart-4: #5089a0;
|
||||
--chart-5: #c4a7e7;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="tokyo-night"] {
|
||||
--background: #e6e7ed;
|
||||
--foreground: #343b59;
|
||||
--card: #f2f3f7;
|
||||
--card-foreground: #343b59;
|
||||
--popover: #f2f3f7;
|
||||
--popover-foreground: #343b59;
|
||||
--content-background: #d6d8df;
|
||||
--primary: #2959aa;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #d6d8df;
|
||||
--secondary-foreground: #343b59;
|
||||
--muted: #d6d8df;
|
||||
--muted-foreground: #484c61;
|
||||
--accent: #ccd3e4;
|
||||
--accent-foreground: #2959aa;
|
||||
--highlight: #8f5e15;
|
||||
--highlight-soft: rgba(143, 94, 21, 0.15);
|
||||
--destructive: #8c4351;
|
||||
--border: #c1c2c7;
|
||||
--input: #83848a;
|
||||
--ring: #2959aa;
|
||||
--chart-1: #2959aa;
|
||||
--chart-2: #8f5e15;
|
||||
--chart-3: #006c86;
|
||||
--chart-4: #385f0d;
|
||||
--chart-5: #65359d;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="tokyo-night"] {
|
||||
--background: #1a1b26;
|
||||
--foreground: #c0caf5;
|
||||
--card: #1f2130;
|
||||
--card-foreground: #c0caf5;
|
||||
--popover: #1f2130;
|
||||
--popover-foreground: #c0caf5;
|
||||
--content-background: #16161e;
|
||||
--primary: #7aa2f7;
|
||||
--primary-foreground: #1a1b26;
|
||||
--secondary: #16161e;
|
||||
--secondary-foreground: #c0caf5;
|
||||
--muted: #16161e;
|
||||
--muted-foreground: #a9b1d6;
|
||||
--accent: rgba(122, 162, 247, 0.16);
|
||||
--accent-foreground: #7aa2f7;
|
||||
--highlight: #e0af68;
|
||||
--highlight-soft: rgba(224, 175, 104, 0.15);
|
||||
--destructive: #f7768e;
|
||||
--border: #363b54;
|
||||
--input: #62677f;
|
||||
--ring: #7aa2f7;
|
||||
--chart-1: #7aa2f7;
|
||||
--chart-2: #e0af68;
|
||||
--chart-3: #7dcfff;
|
||||
--chart-4: #9ece6a;
|
||||
--chart-5: #bb9af7;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="catppuccin"] {
|
||||
--background: #e6e9ef;
|
||||
--foreground: #484b64;
|
||||
--card: #eff1f5;
|
||||
--card-foreground: #484b64;
|
||||
--popover: #eff1f5;
|
||||
--popover-foreground: #484b64;
|
||||
--content-background: #dce0e8;
|
||||
--primary: #8839ef;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #dce0e8;
|
||||
--secondary-foreground: #484b64;
|
||||
--muted: #dce0e8;
|
||||
--muted-foreground: #64677c;
|
||||
--accent: #d9d0ef;
|
||||
--accent-foreground: #7632d0;
|
||||
--highlight: #b24608;
|
||||
--highlight-soft: rgba(178, 70, 8, 0.15);
|
||||
--destructive: #d00f38;
|
||||
--border: #ccd0da;
|
||||
--input: #83858e;
|
||||
--ring: #8839ef;
|
||||
--chart-1: #8839ef;
|
||||
--chart-2: #b24608;
|
||||
--chart-3: #1c5fe4;
|
||||
--chart-4: #307820;
|
||||
--chart-5: #9a4e86;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="catppuccin"] {
|
||||
--background: #1e1e2e;
|
||||
--foreground: #cdd6f4;
|
||||
--card: #313244;
|
||||
--card-foreground: #cdd6f4;
|
||||
--popover: #313244;
|
||||
--popover-foreground: #cdd6f4;
|
||||
--content-background: #181825;
|
||||
--primary: #cba6f7;
|
||||
--primary-foreground: #1e1e2e;
|
||||
--secondary: #181825;
|
||||
--secondary-foreground: #cdd6f4;
|
||||
--muted: #181825;
|
||||
--muted-foreground: #a6adc8;
|
||||
--accent: rgba(203, 166, 247, 0.16);
|
||||
--accent-foreground: #cba6f7;
|
||||
--highlight: #fab387;
|
||||
--highlight-soft: rgba(250, 179, 135, 0.15);
|
||||
--destructive: #f38ba8;
|
||||
--border: #45475a;
|
||||
--input: #66697c;
|
||||
--ring: #cba6f7;
|
||||
--chart-1: #cba6f7;
|
||||
--chart-2: #fab387;
|
||||
--chart-3: #89b4fa;
|
||||
--chart-4: #a6e3a1;
|
||||
--chart-5: #f5c2e7;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="solarized"] {
|
||||
--background: #fdf6e3;
|
||||
--foreground: #47565c;
|
||||
--card: #fffdf6;
|
||||
--card-foreground: #47565c;
|
||||
--popover: #fffdf6;
|
||||
--popover-foreground: #47565c;
|
||||
--content-background: #eee8d5;
|
||||
--primary: #268bd2;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eee8d5;
|
||||
--secondary-foreground: #47565c;
|
||||
--muted: #eee8d5;
|
||||
--muted-foreground: #687272;
|
||||
--accent: #dfe7e1;
|
||||
--accent-foreground: #1d6ba2;
|
||||
--highlight: #c34815;
|
||||
--highlight-soft: rgba(195, 72, 21, 0.15);
|
||||
--destructive: #d5302e;
|
||||
--border: #e6dfc8;
|
||||
--input: #928f83;
|
||||
--ring: #268bd2;
|
||||
--chart-1: #268bd2;
|
||||
--chart-2: #c34815;
|
||||
--chart-3: #217e77;
|
||||
--chart-4: #687700;
|
||||
--chart-5: #666ab8;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="solarized"] {
|
||||
--background: #002b36;
|
||||
--foreground: #a8b4b6;
|
||||
--card: #073642;
|
||||
--card-foreground: #a8b4b6;
|
||||
--popover: #073642;
|
||||
--popover-foreground: #a8b4b6;
|
||||
--content-background: #001f28;
|
||||
--primary: #268bd2;
|
||||
--primary-foreground: #002b36;
|
||||
--secondary: #001f28;
|
||||
--secondary-foreground: #a8b4b6;
|
||||
--muted: #001f28;
|
||||
--muted-foreground: #809196;
|
||||
--accent: rgba(38, 139, 210, 0.16);
|
||||
--accent-foreground: #56a5dc;
|
||||
--highlight: #d67147;
|
||||
--highlight-soft: rgba(214, 113, 71, 0.15);
|
||||
--destructive: #e56563;
|
||||
--border: #0d4552;
|
||||
--input: #4e767f;
|
||||
--ring: #268bd2;
|
||||
--chart-1: #268bd2;
|
||||
--chart-2: #d67147;
|
||||
--chart-3: #2aa198;
|
||||
--chart-4: #859900;
|
||||
--chart-5: #8488cd;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="ayu"] {
|
||||
--background: #f8f9fa;
|
||||
--foreground: #52565b;
|
||||
--card: #fcfcfc;
|
||||
--card-foreground: #52565b;
|
||||
--popover: #fcfcfc;
|
||||
--popover-foreground: #52565b;
|
||||
--content-background: #ebeef0;
|
||||
--primary: #cb7f14;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #ebeef0;
|
||||
--secondary-foreground: #52565b;
|
||||
--muted: #ebeef0;
|
||||
--muted-foreground: #697381;
|
||||
--accent: #f2e8da;
|
||||
--accent-foreground: #945d0f;
|
||||
--highlight: #af5d23;
|
||||
--highlight-soft: rgba(175, 93, 35, 0.15);
|
||||
--destructive: #b45555;
|
||||
--border: #dfe2e5;
|
||||
--input: #8d9092;
|
||||
--ring: #cb7f14;
|
||||
--chart-1: #cb7f14;
|
||||
--chart-2: #af5d23;
|
||||
--chart-3: #3a7a90;
|
||||
--chart-4: #5e7d00;
|
||||
--chart-5: #8664a7;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="ayu"] {
|
||||
--background: #0d1017;
|
||||
--foreground: #bfbdb6;
|
||||
--card: #10141c;
|
||||
--card-foreground: #bfbdb6;
|
||||
--popover: #10141c;
|
||||
--popover-foreground: #bfbdb6;
|
||||
--content-background: #070a0f;
|
||||
--primary: #e6b450;
|
||||
--primary-foreground: #0d1017;
|
||||
--secondary: #070a0f;
|
||||
--secondary-foreground: #bfbdb6;
|
||||
--muted: #070a0f;
|
||||
--muted-foreground: #747c8e;
|
||||
--accent: rgba(230, 180, 80, 0.16);
|
||||
--accent-foreground: #e6b450;
|
||||
--highlight: #ff8f40;
|
||||
--highlight-soft: rgba(255, 143, 64, 0.15);
|
||||
--destructive: #f07178;
|
||||
--border: #1b1f29;
|
||||
--input: #5f6167;
|
||||
--ring: #e6b450;
|
||||
--chart-1: #e6b450;
|
||||
--chart-2: #ff8f40;
|
||||
--chart-3: #39bae6;
|
||||
--chart-4: #aad94c;
|
||||
--chart-5: #d2a6ff;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="kanagawa"] {
|
||||
--background: #e5ddb0;
|
||||
--foreground: #434350;
|
||||
--card: #f2ecbc;
|
||||
--card-foreground: #434350;
|
||||
--popover: #f2ecbc;
|
||||
--popover-foreground: #434350;
|
||||
--content-background: #dcd5ac;
|
||||
--primary: #624c83;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #dcd5ac;
|
||||
--secondary-foreground: #434350;
|
||||
--muted: #dcd5ac;
|
||||
--muted-foreground: #636155;
|
||||
--accent: #d3c9aa;
|
||||
--accent-foreground: #604a80;
|
||||
--highlight: #934e00;
|
||||
--highlight-soft: rgba(147, 78, 0, 0.15);
|
||||
--destructive: #ac3747;
|
||||
--border: #d5cea3;
|
||||
--input: #807c65;
|
||||
--ring: #624c83;
|
||||
--chart-1: #624c83;
|
||||
--chart-2: #934e00;
|
||||
--chart-3: #47618f;
|
||||
--chart-4: #53673b;
|
||||
--chart-5: #914a62;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="kanagawa"] {
|
||||
--background: #1f1f28;
|
||||
--foreground: #dcd7ba;
|
||||
--card: #2a2a37;
|
||||
--card-foreground: #dcd7ba;
|
||||
--popover: #2a2a37;
|
||||
--popover-foreground: #dcd7ba;
|
||||
--content-background: #16161d;
|
||||
--primary: #7e9cd8;
|
||||
--primary-foreground: #1f1f28;
|
||||
--secondary: #16161d;
|
||||
--secondary-foreground: #dcd7ba;
|
||||
--muted: #16161d;
|
||||
--muted-foreground: #898881;
|
||||
--accent: rgba(126, 156, 216, 0.16);
|
||||
--accent-foreground: #7e9cd8;
|
||||
--highlight: #ff9e3b;
|
||||
--highlight-soft: rgba(255, 158, 59, 0.15);
|
||||
--destructive: #ed5050;
|
||||
--border: #363646;
|
||||
--input: #696970;
|
||||
--ring: #7e9cd8;
|
||||
--chart-1: #7e9cd8;
|
||||
--chart-2: #ff9e3b;
|
||||
--chart-3: #7fb4ca;
|
||||
--chart-4: #98bb6c;
|
||||
--chart-5: #d27e99;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="everforest"] {
|
||||
--background: #efebd4;
|
||||
--foreground: #444e54;
|
||||
--card: #fdf6e3;
|
||||
--card-foreground: #444e54;
|
||||
--popover: #fdf6e3;
|
||||
--popover-foreground: #444e54;
|
||||
--content-background: #e6e2cc;
|
||||
--primary: #7d8f01;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #e6e2cc;
|
||||
--secondary-foreground: #444e54;
|
||||
--muted: #e6e2cc;
|
||||
--muted-foreground: #646c63;
|
||||
--accent: #dfdeb6;
|
||||
--accent-foreground: #5a6701;
|
||||
--highlight: #a45419;
|
||||
--highlight-soft: rgba(164, 84, 25, 0.15);
|
||||
--destructive: #b83f3d;
|
||||
--border: #bdc3af;
|
||||
--input: #84887c;
|
||||
--ring: #7d8f01;
|
||||
--chart-1: #7d8f01;
|
||||
--chart-2: #a45419;
|
||||
--chart-3: #2c7096;
|
||||
--chart-4: #616f01;
|
||||
--chart-5: #9e4b84;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="everforest"] {
|
||||
--background: #2d353b;
|
||||
--foreground: #d3c6aa;
|
||||
--card: #343f44;
|
||||
--card-foreground: #d3c6aa;
|
||||
--popover: #343f44;
|
||||
--popover-foreground: #d3c6aa;
|
||||
--content-background: #232a2e;
|
||||
--primary: #a7c080;
|
||||
--primary-foreground: #2d353b;
|
||||
--secondary: #232a2e;
|
||||
--secondary-foreground: #d3c6aa;
|
||||
--muted: #232a2e;
|
||||
--muted-foreground: #949f97;
|
||||
--accent: rgba(167, 192, 128, 0.16);
|
||||
--accent-foreground: #a7c080;
|
||||
--highlight: #e69875;
|
||||
--highlight-soft: rgba(230, 152, 117, 0.15);
|
||||
--destructive: #e67e80;
|
||||
--border: #475258;
|
||||
--input: #787e7e;
|
||||
--ring: #a7c080;
|
||||
--chart-1: #a7c080;
|
||||
--chart-2: #e69875;
|
||||
--chart-3: #7fbbb3;
|
||||
--chart-4: #a7c080;
|
||||
--chart-5: #d699b6;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
|
||||
:root[data-palette="primer"] {
|
||||
--background: #f6f8fa;
|
||||
--foreground: #25292e;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #25292e;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #25292e;
|
||||
--content-background: #eff2f5;
|
||||
--primary: #0969da;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eff2f5;
|
||||
--secondary-foreground: #25292e;
|
||||
--muted: #eff2f5;
|
||||
--muted-foreground: #59636e;
|
||||
--accent: #d5e4f6;
|
||||
--accent-foreground: #0861c9;
|
||||
--highlight: #9a6700;
|
||||
--highlight-soft: rgba(154, 103, 0, 0.15);
|
||||
--destructive: #cf222e;
|
||||
--border: #d1d9e0;
|
||||
--input: #898f94;
|
||||
--ring: #0969da;
|
||||
--chart-1: #0969da;
|
||||
--chart-2: #9a6700;
|
||||
--chart-3: #0550ae;
|
||||
--chart-4: #116329;
|
||||
--chart-5: #8250df;
|
||||
--destructive-foreground: #ffffff;
|
||||
}
|
||||
|
||||
.dark[data-palette="primer"] {
|
||||
--background: #0d1117;
|
||||
--foreground: #f0f6fc;
|
||||
--card: #151b23;
|
||||
--card-foreground: #f0f6fc;
|
||||
--popover: #151b23;
|
||||
--popover-foreground: #f0f6fc;
|
||||
--content-background: #010409;
|
||||
--primary: #58a6ff;
|
||||
--primary-foreground: #0d1117;
|
||||
--secondary: #010409;
|
||||
--secondary-foreground: #f0f6fc;
|
||||
--muted: #010409;
|
||||
--muted-foreground: #9198a1;
|
||||
--accent: rgba(88, 166, 255, 0.16);
|
||||
--accent-foreground: #58a6ff;
|
||||
--highlight: #e3b341;
|
||||
--highlight-soft: rgba(227, 179, 65, 0.15);
|
||||
--destructive: #ff7b72;
|
||||
--border: #3d444d;
|
||||
--input: #5a6169;
|
||||
--ring: #58a6ff;
|
||||
--chart-1: #58a6ff;
|
||||
--chart-2: #e3b341;
|
||||
--chart-3: #79c0ff;
|
||||
--chart-4: #56d364;
|
||||
--chart-5: #d2a8ff;
|
||||
--destructive-foreground: #0b1720;
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*/
|
||||
|
||||
import { applyPalette, DEFAULT_PALETTE, isPaletteId, type PaletteId } from '@/lib/palettes';
|
||||
import { create } from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
@@ -12,6 +13,8 @@ type Theme = 'light' | 'dark';
|
||||
|
||||
interface UIState {
|
||||
theme: Theme;
|
||||
/** INBUXA: the color palette, the same set INBUXA webmail offers. */
|
||||
palette: PaletteId;
|
||||
sidebarOpen: boolean;
|
||||
/** INBUXA: the sidebar folded to a rail of icon tiles, on wide screens. */
|
||||
sidebarCollapsed: boolean;
|
||||
@@ -19,6 +22,7 @@ interface UIState {
|
||||
|
||||
toggleTheme: () => void;
|
||||
setTheme: (theme: Theme) => void;
|
||||
setPalette: (palette: PaletteId) => void;
|
||||
toggleSidebar: () => void;
|
||||
setSidebarOpen: (open: boolean) => void;
|
||||
toggleSidebarCollapsed: () => void;
|
||||
@@ -40,6 +44,7 @@ export const useUIStore = create<UIState>()(
|
||||
typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light',
|
||||
sidebarOpen: typeof window !== 'undefined' ? (window.matchMedia?.('(min-width: 768px)').matches ?? true) : true,
|
||||
sidebarCollapsed: false,
|
||||
palette: DEFAULT_PALETTE,
|
||||
activeSection: '',
|
||||
|
||||
toggleTheme: () => {
|
||||
@@ -61,6 +66,11 @@ export const useUIStore = create<UIState>()(
|
||||
set({ sidebarOpen: open });
|
||||
},
|
||||
|
||||
setPalette: (palette) => {
|
||||
applyPalette(palette);
|
||||
set({ palette });
|
||||
},
|
||||
|
||||
toggleSidebarCollapsed: () => {
|
||||
set({ sidebarCollapsed: !get().sidebarCollapsed });
|
||||
},
|
||||
@@ -73,12 +83,14 @@ export const useUIStore = create<UIState>()(
|
||||
name: 'inbuxa-ui',
|
||||
partialize: (state) => ({
|
||||
theme: state.theme,
|
||||
palette: state.palette,
|
||||
sidebarCollapsed: state.sidebarCollapsed,
|
||||
}),
|
||||
onRehydrateStorage: () => {
|
||||
return (state) => {
|
||||
if (state) {
|
||||
applyThemeClass(state.theme);
|
||||
applyPalette(isPaletteId(state.palette) ? state.palette : DEFAULT_PALETTE);
|
||||
}
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user