diff --git a/.palette-sources/palettes-upstream.md b/.palette-sources/palettes-upstream.md index 0c85ccd..5a530c5 100644 --- a/.palette-sources/palettes-upstream.md +++ b/.palette-sources/palettes-upstream.md @@ -47,3 +47,104 @@ bg #e6e7ed · bg_dark #d6d8df · fg #343b59 · line numbers #9da0ab · border #c link #2959aa accents: purple #65359d · red #8c4351 · cyan #006c86 · blue #2959aa yellow #8f5e15 · teal #33635c · green #385f0d + +--- + +Fetched 2026-09-06 from the projects' own repositories, same rule as above. +Where a project publishes fewer background tiers than ihasmail needs, the +missing one is derived and marked **derived** here rather than passed off as +upstream. Body text is lifted to 7:1 by the build script for most of these — +they target their own ~4.5:1 — and every shift is printed in the generated CSS. + +## Catppuccin — catppuccin/palette, MIT (palette.json) +Cited from the palette repo rather than the hub README; it is the normative +machine-readable source. + +### Mocha (dark) +base #1e1e2e · mantle #181825 · crust #11111b · surface0 #313244 · surface1 #45475a +text #cdd6f4 · subtext0 #a6adc8 · overlay1 #7f849c +mauve #cba6f7 · blue #89b4fa · red #f38ba8 · peach #fab387 · green #a6e3a1 +yellow #f9e2af · pink #f5c2e7 + +### Latte (light) +base #eff1f5 · mantle #e6e9ef · crust #dce0e8 · surface0 #ccd0da · surface1 #bcc0cc +text #4c4f69 · subtext0 #6c6f85 +mauve #8839ef · blue #1e66f5 · red #d20f39 · peach #fe640b · green #40a02b +yellow #df8e1d · pink #ea76cb + +Latte publishes no tier lighter than `base`, so `base` is used as the elevated +surface and `mantle` as the page behind it. + +## Solarized — altercation/solarized, MIT (README "The Values") +base03 #002b36 · base02 #073642 · base01 #586e75 · base00 #657b83 +base0 #839496 · base1 #93a1a1 · base2 #eee8d5 · base3 #fdf6e3 +yellow #b58900 · orange #cb4b16 · red #dc322f · magenta #d33682 +violet #6c71c4 · blue #268bd2 · cyan #2aa198 · green #859900 + +The accents are shared by both modes by design. Two tiers are **derived**: the +sunken dark surface #001f28 (below base03) and the raised light surface +#fffdf6 (above base3), neither of which Solarized publishes, plus the two +rule colours #0d4552 and #e6dfc8. + +## Everforest — sainnhe/everforest, MIT (palette.md), medium contrast +### Dark +bg_dim #232a2e · bg0 #2d353b · bg1 #343f44 · bg3 #475258 +fg #d3c6aa · grey1 #859289 +red #e67e80 · orange #e69875 · yellow #dbbc7f · green #a7c080 · aqua #83c092 +blue #7fbbb3 · purple #d699b6 + +### Light +bg_dim #efebd4 · bg0 #fdf6e3 · bg3 #e6e2cc · bg5 #bdc3af +fg #5c6a72 · grey1 #939f91 +red #f85552 · orange #f57d26 · yellow #dfa000 · green #8da101 · aqua #35a77c +blue #3a94c5 · purple #df69ba + +Light uses bg_dim as the page and bg0 as the raised surface, so the card the +reader looks at is the colour Everforest calls its background. + +## Kanagawa — rebelot/kanagawa.nvim, MIT (lua/kanagawa/colors.lua) +### Wave (dark) +sumiInk0 #16161D · sumiInk3 #1F1F28 · sumiInk4 #2A2A37 · sumiInk5 #363646 +fujiWhite #DCD7BA · fujiGray #727169 +crystalBlue #7E9CD8 · springBlue #7FB4CA · samuraiRed #E82424 · roninYellow #FF9E3B +springGreen #98BB6C · carpYellow #E6C384 · sakuraPink #D27E99 + +### Lotus (light) +lotusWhite0 #d5cea3 · lotusWhite1 #dcd5ac · lotusWhite2 #e5ddb0 · lotusWhite3 #f2ecbc +lotusInk1 #545464 · lotusGray2 #716e61 +lotusViolet4 #624c83 · lotusBlue4 #4d699b · lotusRed #c84053 · lotusOrange #cc6d00 +lotusGreen #6f894e · lotusYellow #77713f · lotusPink #b35b79 + +## Ayu — ayu-theme/ayu-colors, MIT (themes/dark.yaml, themes/light.yaml) +The YAMLs give the base palette and the surfaces as literals but express syntax +roles as references (`$palette.indigo.l2`), and the resolved files are not +committed. The two signature accents are taken from the same organisation's +MIT-licensed ayu-theme/vscode-ayu build. + +### Dark +surface base #0D1017 · lift #10141C (sunk is `base -L0.1`, **derived** here as #070a0f) +ui line #1B1F29 · ui fg #5A6378 · editor fg #BFBDB6 +red #F07178 · orange #FF8F40 · yellow #FFB454 · green #AAD94C · teal #95E6CB +indigo #39BAE6 · blue #59C2FF · purple #D2A6FF · accent #E6B450 (vscode-ayu) + +### Light +surface sunk #EBEEF0 · base #F8F9FA · lift #FCFCFC +ui fg #828E9F · editor fg #5C6166 · rule #dfe2e5 (**derived**) +red #F07171 · orange #FA8532 · yellow #EBA400 · green #86B300 · teal #4CBF99 +indigo #55B4D4 · blue #22A4E6 · purple #A37ACC · accent #F29718 (vscode-ayu) + +## Primer — primer/primitives, MIT (src/tokens/base/color/{dark,light}) +Named "Primer" after the design system. The colour values are MIT; "GitHub" +and the Invertocat are trademarks, and nothing here is endorsed by them. + +### Dark +neutral #0D1117 #151B23 #212830 #262C36 #2A313C #2F3742 #3D444D #656C76 + #9198A1 #B7BDC8 #D1D7E0 #F0F6FC · black #010409 +blue #79c0ff #58a6ff · green #56d364 #3fb950 · yellow #e3b341 #d29922 +red #ff7b72 · purple #d2a8ff + +### Light +neutral #F6F8FA #EFF2F5 #E6EAEF #E0E6EB #DAE0E7 #D1D9E0 #C8D1DA #818B98 + #59636E #454C54 #393F46 #25292E +blue #0969da #0550ae · green #1a7f37 #116329 · yellow #bf8700 #9a6700 +red #cf222e · purple #8250df diff --git a/NOTICE b/NOTICE index 2d15f22..6edaa5e 100644 --- a/NOTICE +++ b/NOTICE @@ -5,7 +5,7 @@ other people that ships inside it and the terms it comes under. ## Colour palettes -Four of the palettes offered in Settings › Appearance are the work of their own +Ten of the palettes offered in Settings › Appearance are the work of their own projects and are used under the MIT licence. Only the published colour values are used — no code, and nothing from anyone else's reimplementation of them. The values as fetched from each project are recorded in @@ -35,9 +35,47 @@ Licensed under the MIT licence. The light variant is "Dawn". Copyright (c) 2019 enkia — https://github.com/enkia/tokyo-night-vscode-theme Licensed under the MIT licence. The light variant is "Day". +### Catppuccin + +Copyright (c) 2021 Catppuccin — https://github.com/catppuccin/palette +Licensed under the MIT licence. "Mocha" is the dark variant and "Latte" the +light one; both are published in that repository's palette.json. + +### Solarized + +Copyright (c) 2011 Ethan Schoonover — https://github.com/altercation/solarized +Licensed under the MIT licence. Light and dark are both original to it, and +share one set of accent values by design. + +### Ayu + +Copyright (c) Konstantin Pschera — https://github.com/ayu-theme/ayu-colors +Licensed under the MIT licence. The two signature accent colours come from the +same author's ayu-theme/vscode-ayu, also MIT. + +### Kanagawa + +Copyright (c) 2021 Tommaso Laurenzi — https://github.com/rebelot/kanagawa.nvim +Licensed under the MIT licence. "Wave" is the dark variant and "Lotus" the +light one. The theme takes its name from Hokusai's print. + +### Everforest + +Copyright (c) 2019 Sainnhe Park — https://github.com/sainnhe/everforest +Licensed under the MIT licence. The medium-contrast variant of each mode is +the one used here. + +### Primer + +Copyright (c) GitHub, Inc. — https://github.com/primer/primitives +Licensed under the MIT licence, which covers the colour values. "GitHub" and +the Invertocat logo are trademarks of GitHub, Inc.; this palette is named +"Primer" after the design system and is neither affiliated with nor endorsed +by GitHub. + --- -The MIT licence, under which all four are used: +The MIT licence, under which all ten are used: Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), diff --git a/scripts/build-palettes.py b/scripts/build-palettes.py index 3b2f092..7f3a36d 100644 --- a/scripts/build-palettes.py +++ b/scripts/build-palettes.py @@ -163,6 +163,90 @@ SOURCES = { q1="#006c86", q2="#385f0d", q3="#65359d", ), }, + "catppuccin": { + "dark": dict( # Mocha + bg="#1e1e2e", elev="#313244", sunken="#181825", line="#45475a", + fg="#cdd6f4", muted="#a6adc8", accent="#cba6f7", link="#89b4fa", + danger="#f38ba8", warn="#fab387", success="#a6e3a1", star="#f9e2af", + q1="#89b4fa", q2="#a6e3a1", q3="#f5c2e7", + ), + "light": dict( # Latte + bg="#e6e9ef", elev="#eff1f5", sunken="#dce0e8", line="#ccd0da", + fg="#4c4f69", muted="#6c6f85", accent="#8839ef", link="#1e66f5", + danger="#d20f39", warn="#fe640b", success="#40a02b", star="#df8e1d", + q1="#1e66f5", q2="#40a02b", q3="#ea76cb", + ), + }, + "solarized": { + "dark": dict( + bg="#002b36", elev="#073642", sunken="#001f28", line="#0d4552", + fg="#839496", muted="#586e75", accent="#268bd2", link="#2aa198", + danger="#dc322f", warn="#cb4b16", success="#859900", star="#b58900", + q1="#2aa198", q2="#859900", q3="#6c71c4", + ), + "light": dict( + bg="#fdf6e3", elev="#fffdf6", sunken="#eee8d5", line="#e6dfc8", + fg="#657b83", muted="#93a1a1", accent="#268bd2", link="#2aa198", + danger="#dc322f", warn="#cb4b16", success="#859900", star="#b58900", + q1="#2aa198", q2="#859900", q3="#6c71c4", + ), + }, + "ayu": { + "dark": dict( + bg="#0d1017", elev="#10141c", sunken="#070a0f", line="#1b1f29", + fg="#bfbdb6", muted="#5a6378", accent="#e6b450", link="#59c2ff", + danger="#f07178", warn="#ff8f40", success="#aad94c", star="#ffb454", + q1="#39bae6", q2="#aad94c", q3="#d2a6ff", + ), + "light": dict( + bg="#f8f9fa", elev="#fcfcfc", sunken="#ebeef0", line="#dfe2e5", + fg="#5c6166", muted="#828e9f", accent="#f29718", link="#22a4e6", + danger="#f07171", warn="#fa8532", success="#86b300", star="#eba400", + q1="#55b4d4", q2="#86b300", q3="#a37acc", + ), + }, + "kanagawa": { + "dark": dict( # Wave + bg="#1f1f28", elev="#2a2a37", sunken="#16161d", line="#363646", + fg="#dcd7ba", muted="#727169", accent="#7e9cd8", link="#7fb4ca", + danger="#e82424", warn="#ff9e3b", success="#98bb6c", star="#e6c384", + q1="#7fb4ca", q2="#98bb6c", q3="#d27e99", + ), + "light": dict( # Lotus + bg="#e5ddb0", elev="#f2ecbc", sunken="#dcd5ac", line="#d5cea3", + fg="#545464", muted="#716e61", accent="#624c83", link="#4d699b", + danger="#c84053", warn="#cc6d00", success="#6f894e", star="#77713f", + q1="#4d699b", q2="#6f894e", q3="#b35b79", + ), + }, + "everforest": { + "dark": dict( # medium + bg="#2d353b", elev="#343f44", sunken="#232a2e", line="#475258", + fg="#d3c6aa", muted="#859289", accent="#a7c080", link="#7fbbb3", + danger="#e67e80", warn="#e69875", success="#a7c080", star="#dbbc7f", + q1="#7fbbb3", q2="#a7c080", q3="#d699b6", + ), + "light": dict( # medium + bg="#efebd4", elev="#fdf6e3", sunken="#e6e2cc", line="#bdc3af", + fg="#5c6a72", muted="#939f91", accent="#8da101", link="#3a94c5", + danger="#f85552", warn="#f57d26", success="#8da101", star="#dfa000", + q1="#3a94c5", q2="#8da101", q3="#df69ba", + ), + }, + "primer": { + "dark": dict( + bg="#0d1117", elev="#151b23", sunken="#010409", line="#3d444d", + fg="#f0f6fc", muted="#9198a1", accent="#58a6ff", link="#79c0ff", + danger="#ff7b72", warn="#e3b341", success="#3fb950", star="#d29922", + q1="#79c0ff", q2="#56d364", q3="#d2a8ff", + ), + "light": dict( + bg="#f6f8fa", elev="#ffffff", sunken="#eff2f5", line="#d1d9e0", + fg="#25292e", muted="#59636e", accent="#0969da", link="#0550ae", + danger="#cf222e", warn="#9a6700", success="#1a7f37", star="#bf8700", + q1="#0550ae", q2="#116329", q3="#8250df", + ), + }, } # What each token has to clear, and against which surface. Normal text is 4.5; @@ -183,6 +267,15 @@ def build(pid: str, mode: str, src: dict[str, str]) -> tuple[dict[str, str], lis notes.append(f"{name} {colour} -> {out} ({contrast(colour, bg):.2f} -> {contrast(out, bg):.2f})") return out + # Body text is lifted like every other text tone rather than exempted. + # Most of these palettes publish a body colour around 4.5:1 -- their own + # target -- and ihasmail asks 7:1 of the text a reader looks at all day. + # Rejecting a palette over that would have cost five of the six added in + # 2026-09; nudging the published colour along its own hue costs nothing a + # reader can name, and the shift is recorded in the header of the + # generated block like every other one. + fg = lift("fg", fg, TEXT_ON_BG["fg"]) + muted = lift("muted", src["muted"], TEXT_ON_BG["muted"]) # Between muted and the background, but still readable: this is timestamps # and counts, which are small and still prose. diff --git a/web/src/lib/__tests__/palette.test.ts b/web/src/lib/__tests__/palette.test.ts index 53a5d92..be3c371 100644 --- a/web/src/lib/__tests__/palette.test.ts +++ b/web/src/lib/__tests__/palette.test.ts @@ -6,7 +6,10 @@ describe("the palettes themselves", () => { // The reason there is no "this palette is dark only" machinery: there is // no such palette. ihasmail's own gained a light half, and the override, // the toggle's memory and a greyed-out control all went with it. - expect(PALETTES.map((p) => p.id)).toEqual(["default", "ihasmail", "dracula", "gruvbox", "rose-pine", "tokyo-night"]); + expect(PALETTES.map((p) => p.id)).toEqual([ + "default", "ihasmail", "dracula", "gruvbox", "rose-pine", "tokyo-night", + "catppuccin", "solarized", "ayu", "kanagawa", "everforest", "primer", + ]); }); it("credits every borrowed palette and neither of ihasmail's own", () => { diff --git a/web/src/lib/palette.ts b/web/src/lib/palette.ts index a1ec3cb..cbca10f 100644 --- a/web/src/lib/palette.ts +++ b/web/src/lib/palette.ts @@ -13,7 +13,9 @@ * the derivation can be checked rather than taken on trust. */ -export type PaletteId = "default" | "ihasmail" | "dracula" | "gruvbox" | "rose-pine" | "tokyo-night"; +export type PaletteId = + | "default" | "ihasmail" | "dracula" | "gruvbox" | "rose-pine" | "tokyo-night" + | "catppuccin" | "solarized" | "ayu" | "kanagawa" | "everforest" | "primer"; export type Mode = "system" | "light" | "dark"; /** What a mode resolves to once the system has been asked. */ export type ResolvedMode = "light" | "dark"; @@ -26,11 +28,11 @@ export interface PaletteMeta { /** * Whether the name is a word rather than a name. * - * Five of these six are proper names -- ihasmail, Dracula, Gruvbox, Rosé - * Pine, Tokyo Night -- and are rendered translate="no" so a page translator - * leaves them alone. "Classic" is not a name, it is an adjective describing - * the theme, and a German reader should see "Klassisch". Reported by a - * native speaker reviewing the German catalogue (#247). + * All but one of these are proper names -- ihasmail, Dracula, Gruvbox and + * the rest -- and are rendered translate="no" so a page translator leaves + * them alone. "Classic" is not a name, it is an adjective describing the + * theme, and a German reader should see "Klassisch". Reported by a native + * speaker reviewing the German catalogue (#247). */ translatable?: boolean; } @@ -42,6 +44,14 @@ export const PALETTES: PaletteMeta[] = [ { id: "gruvbox", name: "Gruvbox", credit: "gruvbox by morhetz (MIT)" }, { id: "rose-pine", name: "Rosé Pine", credit: "Rosé Pine (MIT) — light variant is Dawn" }, { id: "tokyo-night", name: "Tokyo Night", credit: "Tokyo Night by enkia (MIT) — light variant is Day" }, + { id: "catppuccin", name: "Catppuccin", credit: "Catppuccin (MIT) — dark is Mocha, light is Latte" }, + { id: "solarized", name: "Solarized", credit: "Solarized by Ethan Schoonover (MIT) — light and dark are both original" }, + { id: "ayu", name: "Ayu", credit: "Ayu by Konstantin Pschera (MIT)" }, + { id: "kanagawa", name: "Kanagawa", credit: "Kanagawa by rebelot (MIT) — dark is Wave, light is Lotus" }, + { id: "everforest", name: "Everforest", credit: "Everforest by sainnhe (MIT)" }, + // Named for the design system rather than for GitHub: the colours are MIT, + // the name and the logo are trademarks, and nothing here is endorsed. + { id: "primer", name: "Primer", credit: "GitHub's Primer primitives (MIT); not affiliated with or endorsed by GitHub" }, ]; const byId = new Map(PALETTES.map((p) => [p.id, p])); diff --git a/web/src/styles/app.css b/web/src/styles/app.css index 3f390e7..b63ebbf 100644 --- a/web/src/styles/app.css +++ b/web/src/styles/app.css @@ -503,6 +503,456 @@ --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); } +/* catppuccin (light) lifted for contrast: fg #4c4f69 -> #484b64 (6.57 -> 7.00); muted #6c6f85 -> #64677c (4.06 -> 4.58); faint #8b8e9e -> #656873 (2.67 -> 4.57); link #1e66f5 -> #1c5fe4 (4.04 -> 4.54); danger #d20f39 -> #d00f38 (4.46 -> 4.53); warn #fe640b -> #b24608 (2.45 -> 4.58); success #40a02b -> #307820 (2.75 -> 4.50); star #df8e1d -> #b97618 (2.15 -> 3.05); border-strong #b8bcc8 -> #83858e (1.56 -> 3.02); q1 #1e66f5 -> #1c5fe4 (4.04 -> 4.54); q2 #40a02b -> #307820 (2.75 -> 4.50); q3 #ea76cb -> #9a4e86 (2.17 -> 4.53) */ + +:root[data-palette="catppuccin"] { + --bg: #e6e9ef; + --bg-elev: #eff1f5; + --bg-sunken: #dce0e8; + --bg-hover: rgba(72, 75, 100, 0.06); + --bg-active: rgba(72, 75, 100, 0.11); + --fg: #484b64; + --fg-muted: #64677c; + --fg-faint: #656873; + --border: #ccd0da; + --border-strong: #83858e; + --accent: #8839ef; + --accent-fg: #ffffff; + --accent-soft: #d9d0ef; + --accent-soft-fg: #7632d0; + --danger: #d00f38; + --danger-soft: rgba(208, 15, 56, 0.15); + --warn: #b24608; + --warn-soft: rgba(178, 70, 8, 0.15); + --success: #307820; + --success-soft: rgba(48, 120, 32, 0.15); + --link: #1c5fe4; + --unread-bg: #ffffff; + --read-bg: #e1e4eb; + --selected-bg: #d9d0ef; + --focus-ring: 0 0 0 3px rgba(136, 57, 239, 0.4); + --star: #b97618; + --q1: #1c5fe4; + --q2: #307820; + --q3: #9a4e86; + --scrollbar: rgba(100, 103, 124, 0.35); + color-scheme: light; +} + +/* catppuccin (dark) lifted for contrast: faint #7d829a -> #81869d (4.32 -> 4.55); border-strong #595c71 -> #66697c (2.49 -> 3.03) */ + +:root[data-theme="dark"][data-palette="catppuccin"] { + --bg: #1e1e2e; + --bg-elev: #313244; + --bg-sunken: #181825; + --bg-hover: rgba(205, 214, 244, 0.06); + --bg-active: rgba(205, 214, 244, 0.11); + --fg: #cdd6f4; + --fg-muted: #a6adc8; + --fg-faint: #81869d; + --border: #45475a; + --border-strong: #66697c; + --accent: #cba6f7; + --accent-fg: #1e1e2e; + --accent-soft: rgba(203, 166, 247, 0.16); + --accent-soft-fg: #cba6f7; + --danger: #f38ba8; + --danger-soft: rgba(243, 139, 168, 0.15); + --warn: #fab387; + --warn-soft: rgba(250, 179, 135, 0.15); + --success: #a6e3a1; + --success-soft: rgba(166, 227, 161, 0.15); + --link: #89b4fa; + --unread-bg: #313244; + --read-bg: #181825; + --selected-bg: rgba(203, 166, 247, 0.18); + --focus-ring: 0 0 0 3px rgba(203, 166, 247, 0.4); + --star: #f9e2af; + --q1: #89b4fa; + --q2: #a6e3a1; + --q3: #f5c2e7; + --scrollbar: rgba(166, 173, 200, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + +/* solarized (light) lifted for contrast: fg #657b83 -> #47565c (4.13 -> 7.07); muted #93a1a1 -> #687272 (2.48 -> 4.59); faint #959a94 -> #6e726e (2.66 -> 4.53); link #2aa198 -> #217e77 (2.93 -> 4.51); danger #dc322f -> #d5302e (4.29 -> 4.54); warn #cb4b16 -> #c34815 (4.27 -> 4.57); success #859900 -> #687700 (2.97 -> 4.61); star #b58900 -> #b38800 (2.98 -> 3.02); border-strong #cecab8 -> #928f83 (1.53 -> 3.00); q1 #2aa198 -> #217e77 (2.93 -> 4.51); q2 #859900 -> #687700 (2.97 -> 4.61); q3 #6c71c4 -> #666ab8 (4.06 -> 4.51) */ + +:root[data-palette="solarized"] { + --bg: #fdf6e3; + --bg-elev: #fffdf6; + --bg-sunken: #eee8d5; + --bg-hover: rgba(71, 86, 92, 0.06); + --bg-active: rgba(71, 86, 92, 0.11); + --fg: #47565c; + --fg-muted: #687272; + --fg-faint: #6e726e; + --border: #e6dfc8; + --border-strong: #928f83; + --accent: #268bd2; + --accent-fg: #ffffff; + --accent-soft: #dfe7e1; + --accent-soft-fg: #1d6ba2; + --danger: #d5302e; + --danger-soft: rgba(213, 48, 46, 0.15); + --warn: #c34815; + --warn-soft: rgba(195, 72, 21, 0.15); + --success: #687700; + --success-soft: rgba(104, 119, 0, 0.15); + --link: #217e77; + --unread-bg: #ffffff; + --read-bg: #f8f1df; + --selected-bg: #dfe7e1; + --focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4); + --star: #b38800; + --q1: #217e77; + --q2: #687700; + --q3: #666ab8; + --scrollbar: rgba(104, 114, 114, 0.35); + color-scheme: light; +} + +/* solarized (dark) lifted for contrast: fg #839496 -> #a8b4b6 (4.75 -> 7.06); muted #586e75 -> #809196 (2.79 -> 4.58); faint #5a7279 -> #7d9095 (2.94 -> 4.50); danger #dc322f -> #e56563 (3.25 -> 4.56); warn #cb4b16 -> #d67147 (3.26 -> 4.51); border-strong #245661 -> #4e767f (1.84 -> 3.02); q3 #6c71c4 -> #8488cd (3.43 -> 4.56) */ + +:root[data-theme="dark"][data-palette="solarized"] { + --bg: #002b36; + --bg-elev: #073642; + --bg-sunken: #001f28; + --bg-hover: rgba(168, 180, 182, 0.06); + --bg-active: rgba(168, 180, 182, 0.11); + --fg: #a8b4b6; + --fg-muted: #809196; + --fg-faint: #7d9095; + --border: #0d4552; + --border-strong: #4e767f; + --accent: #268bd2; + --accent-fg: #002b36; + --accent-soft: rgba(38, 139, 210, 0.16); + --accent-soft-fg: #56a5dc; + --danger: #e56563; + --danger-soft: rgba(229, 101, 99, 0.15); + --warn: #d67147; + --warn-soft: rgba(214, 113, 71, 0.15); + --success: #859900; + --success-soft: rgba(133, 153, 0, 0.15); + --link: #2aa198; + --unread-bg: #073642; + --read-bg: #001f28; + --selected-bg: rgba(38, 139, 210, 0.18); + --focus-ring: 0 0 0 3px rgba(38, 139, 210, 0.4); + --star: #b58900; + --q1: #2aa198; + --q2: #859900; + --q3: #8488cd; + --scrollbar: rgba(128, 145, 150, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + +/* ayu (light) lifted for contrast: fg #5c6166 -> #52565b (5.93 -> 7.01); muted #828e9f -> #697381 (3.15 -> 4.56); faint #949ba5 -> #6e737a (2.66 -> 4.53); link #22a4e6 -> #1979aa (2.65 -> 4.57); danger #f07171 -> #b45555 (2.73 -> 4.56); warn #fa8532 -> #af5d23 (2.35 -> 4.51); success #86b300 -> #5e7d00 (2.35 -> 4.52); accent #f29718 -> #cb7f14 (2.16 -> 3.03); star #eba400 -> #be8500 (2.02 -> 3.04); border-strong #cacdd0 -> #8d9092 (1.51 -> 3.05); q1 #55b4d4 -> #3a7a90 (2.25 -> 4.56); q2 #86b300 -> #5e7d00 (2.35 -> 4.52); q3 #a37acc -> #8664a7 (3.21 -> 4.53) */ + +:root[data-palette="ayu"] { + --bg: #f8f9fa; + --bg-elev: #fcfcfc; + --bg-sunken: #ebeef0; + --bg-hover: rgba(82, 86, 91, 0.06); + --bg-active: rgba(82, 86, 91, 0.11); + --fg: #52565b; + --fg-muted: #697381; + --fg-faint: #6e737a; + --border: #dfe2e5; + --border-strong: #8d9092; + --accent: #cb7f14; + --accent-fg: #ffffff; + --accent-soft: #f2e8da; + --accent-soft-fg: #945d0f; + --danger: #b45555; + --danger-soft: rgba(180, 85, 85, 0.15); + --warn: #af5d23; + --warn-soft: rgba(175, 93, 35, 0.15); + --success: #5e7d00; + --success-soft: rgba(94, 125, 0, 0.15); + --link: #1979aa; + --unread-bg: #ffffff; + --read-bg: #f3f4f5; + --selected-bg: #f2e8da; + --focus-ring: 0 0 0 3px rgba(203, 127, 20, 0.4); + --star: #be8500; + --q1: #3a7a90; + --q2: #5e7d00; + --q3: #8664a7; + --scrollbar: rgba(105, 115, 129, 0.35); + color-scheme: light; +} + +/* ayu (dark) lifted for contrast: muted #5a6378 -> #747c8e (3.16 -> 4.55); faint #555c6a -> #777d88 (2.83 -> 4.60); border-strong #34373e -> #5f6167 (1.60 -> 3.07) */ + +:root[data-theme="dark"][data-palette="ayu"] { + --bg: #0d1017; + --bg-elev: #10141c; + --bg-sunken: #070a0f; + --bg-hover: rgba(191, 189, 182, 0.06); + --bg-active: rgba(191, 189, 182, 0.11); + --fg: #bfbdb6; + --fg-muted: #747c8e; + --fg-faint: #777d88; + --border: #1b1f29; + --border-strong: #5f6167; + --accent: #e6b450; + --accent-fg: #0d1017; + --accent-soft: rgba(230, 180, 80, 0.16); + --accent-soft-fg: #e6b450; + --danger: #f07178; + --danger-soft: rgba(240, 113, 120, 0.15); + --warn: #ff8f40; + --warn-soft: rgba(255, 143, 64, 0.15); + --success: #aad94c; + --success-soft: rgba(170, 217, 76, 0.15); + --link: #59c2ff; + --unread-bg: #10141c; + --read-bg: #070a0f; + --selected-bg: rgba(230, 180, 80, 0.18); + --focus-ring: 0 0 0 3px rgba(230, 180, 80, 0.4); + --star: #ffb454; + --q1: #39bae6; + --q2: #aad94c; + --q3: #d2a6ff; + --scrollbar: rgba(116, 124, 142, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + +/* kanagawa (light) lifted for contrast: fg #545464 -> #434350 (5.41 -> 7.09); muted #716e61 -> #636155 (3.73 -> 4.54); faint #8a8670 -> #636051 (2.67 -> 4.61); link #4d699b -> #47618f (4.02 -> 4.53); danger #c84053 -> #ac3747 (3.55 -> 4.51); warn #cc6d00 -> #934e00 (2.66 -> 4.59); success #6f894e -> #53673b (2.85 -> 4.54); border-strong #bfb997 -> #807c65 (1.44 -> 3.07); q1 #4d699b -> #47618f (4.02 -> 4.53); q2 #6f894e -> #53673b (2.85 -> 4.54); q3 #b35b79 -> #914a62 (3.26 -> 4.55) */ + +:root[data-palette="kanagawa"] { + --bg: #e5ddb0; + --bg-elev: #f2ecbc; + --bg-sunken: #dcd5ac; + --bg-hover: rgba(67, 67, 80, 0.06); + --bg-active: rgba(67, 67, 80, 0.11); + --fg: #434350; + --fg-muted: #636155; + --fg-faint: #636051; + --border: #d5cea3; + --border-strong: #807c65; + --accent: #624c83; + --accent-fg: #ffffff; + --accent-soft: #d3c9aa; + --accent-soft-fg: #604a80; + --danger: #ac3747; + --danger-soft: rgba(172, 55, 71, 0.15); + --warn: #934e00; + --warn-soft: rgba(147, 78, 0, 0.15); + --success: #53673b; + --success-soft: rgba(83, 103, 59, 0.15); + --link: #47618f; + --unread-bg: #ffffff; + --read-bg: #e0d8ad; + --selected-bg: #d3c9aa; + --focus-ring: 0 0 0 3px rgba(98, 76, 131, 0.4); + --star: #77713f; + --q1: #47618f; + --q2: #53673b; + --q3: #914a62; + --scrollbar: rgba(99, 97, 85, 0.35); + color-scheme: light; +} + +/* kanagawa (dark) lifted for contrast: muted #727169 -> #898881 (3.33 -> 4.59); faint #696866 -> #888886 (2.94 -> 4.60); danger #e82424 -> #ed5050 (3.66 -> 4.56); border-strong #4f4e57 -> #696970 (1.99 -> 3.00) */ + +:root[data-theme="dark"][data-palette="kanagawa"] { + --bg: #1f1f28; + --bg-elev: #2a2a37; + --bg-sunken: #16161d; + --bg-hover: rgba(220, 215, 186, 0.06); + --bg-active: rgba(220, 215, 186, 0.11); + --fg: #dcd7ba; + --fg-muted: #898881; + --fg-faint: #888886; + --border: #363646; + --border-strong: #696970; + --accent: #7e9cd8; + --accent-fg: #1f1f28; + --accent-soft: rgba(126, 156, 216, 0.16); + --accent-soft-fg: #7e9cd8; + --danger: #ed5050; + --danger-soft: rgba(237, 80, 80, 0.15); + --warn: #ff9e3b; + --warn-soft: rgba(255, 158, 59, 0.15); + --success: #98bb6c; + --success-soft: rgba(152, 187, 108, 0.15); + --link: #7fb4ca; + --unread-bg: #2a2a37; + --read-bg: #16161d; + --selected-bg: rgba(126, 156, 216, 0.18); + --focus-ring: 0 0 0 3px rgba(126, 156, 216, 0.4); + --star: #e6c384; + --q1: #7fb4ca; + --q2: #98bb6c; + --q3: #d27e99; + --scrollbar: rgba(137, 136, 129, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + +/* everforest (light) lifted for contrast: fg #5c6a72 -> #444e54 (4.66 -> 7.11); muted #939f91 -> #646c63 (2.30 -> 4.53); faint #8e9285 -> #686b61 (2.65 -> 4.53); link #3a94c5 -> #2c7096 (2.81 -> 4.53); danger #f85552 -> #b83f3d (2.73 -> 4.59); warn #f57d26 -> #a45419 (2.23 -> 4.53); success #8da101 -> #616f01 (2.42 -> 4.63); accent #8da101 -> #7d8f01 (2.42 -> 3.02); star #dfa000 -> #b07e00 (1.91 -> 3.00); border-strong #abb1a1 -> #84887c (1.84 -> 3.02); q1 #3a94c5 -> #2c7096 (2.81 -> 4.53); q2 #8da101 -> #616f01 (2.42 -> 4.63); q3 #df69ba -> #9e4b84 (2.55 -> 4.61) */ + +:root[data-palette="everforest"] { + --bg: #efebd4; + --bg-elev: #fdf6e3; + --bg-sunken: #e6e2cc; + --bg-hover: rgba(68, 78, 84, 0.06); + --bg-active: rgba(68, 78, 84, 0.11); + --fg: #444e54; + --fg-muted: #646c63; + --fg-faint: #686b61; + --border: #bdc3af; + --border-strong: #84887c; + --accent: #7d8f01; + --accent-fg: #ffffff; + --accent-soft: #dfdeb6; + --accent-soft-fg: #5a6701; + --danger: #b83f3d; + --danger-soft: rgba(184, 63, 61, 0.15); + --warn: #a45419; + --warn-soft: rgba(164, 84, 25, 0.15); + --success: #616f01; + --success-soft: rgba(97, 111, 1, 0.15); + --link: #2c7096; + --unread-bg: #ffffff; + --read-bg: #eae6d0; + --selected-bg: #dfdeb6; + --focus-ring: 0 0 0 3px rgba(125, 143, 1, 0.4); + --star: #b07e00; + --q1: #2c7096; + --q2: #616f01; + --q3: #9e4b84; + --scrollbar: rgba(100, 108, 99, 0.35); + color-scheme: light; +} + +/* everforest (dark) lifted for contrast: muted #859289 -> #949f97 (3.84 -> 4.55); faint #757f7b -> #969e9b (3.02 -> 4.55); border-strong #5c6364 -> #787e7e (2.03 -> 3.02) */ + +:root[data-theme="dark"][data-palette="everforest"] { + --bg: #2d353b; + --bg-elev: #343f44; + --bg-sunken: #232a2e; + --bg-hover: rgba(211, 198, 170, 0.06); + --bg-active: rgba(211, 198, 170, 0.11); + --fg: #d3c6aa; + --fg-muted: #949f97; + --fg-faint: #969e9b; + --border: #475258; + --border-strong: #787e7e; + --accent: #a7c080; + --accent-fg: #2d353b; + --accent-soft: rgba(167, 192, 128, 0.16); + --accent-soft-fg: #a7c080; + --danger: #e67e80; + --danger-soft: rgba(230, 126, 128, 0.15); + --warn: #e69875; + --warn-soft: rgba(230, 152, 117, 0.15); + --success: #a7c080; + --success-soft: rgba(167, 192, 128, 0.15); + --link: #7fbbb3; + --unread-bg: #343f44; + --read-bg: #232a2e; + --selected-bg: rgba(167, 192, 128, 0.18); + --focus-ring: 0 0 0 3px rgba(167, 192, 128, 0.4); + --star: #dbbc7f; + --q1: #7fbbb3; + --q2: #a7c080; + --q3: #d699b6; + --scrollbar: rgba(148, 159, 151, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + +/* primer (light) lifted for contrast: faint #889098 -> #6b7278 (3.04 -> 4.58); star #bf8700 -> #bd8600 (2.95 -> 3.00); border-strong #b7bfc5 -> #898f94 (1.75 -> 3.07) */ + +:root[data-palette="primer"] { + --bg: #f6f8fa; + --bg-elev: #ffffff; + --bg-sunken: #eff2f5; + --bg-hover: rgba(37, 41, 46, 0.06); + --bg-active: rgba(37, 41, 46, 0.11); + --fg: #25292e; + --fg-muted: #59636e; + --fg-faint: #6b7278; + --border: #d1d9e0; + --border-strong: #898f94; + --accent: #0969da; + --accent-fg: #ffffff; + --accent-soft: #d5e4f6; + --accent-soft-fg: #0861c9; + --danger: #cf222e; + --danger-soft: rgba(207, 34, 46, 0.15); + --warn: #9a6700; + --warn-soft: rgba(154, 103, 0, 0.15); + --success: #1a7f37; + --success-soft: rgba(26, 127, 55, 0.15); + --link: #0550ae; + --unread-bg: #ffffff; + --read-bg: #f0f2f4; + --selected-bg: #d5e4f6; + --focus-ring: 0 0 0 3px rgba(9, 105, 218, 0.4); + --star: #bd8600; + --q1: #0550ae; + --q2: #116329; + --q3: #8250df; + --scrollbar: rgba(89, 99, 110, 0.35); + color-scheme: light; +} + +/* primer (dark) lifted for contrast: faint #697078 -> #767d84 (3.78 -> 4.54); border-strong #585f67 -> #5a6169 (2.93 -> 3.02) */ + +:root[data-theme="dark"][data-palette="primer"] { + --bg: #0d1117; + --bg-elev: #151b23; + --bg-sunken: #010409; + --bg-hover: rgba(240, 246, 252, 0.06); + --bg-active: rgba(240, 246, 252, 0.11); + --fg: #f0f6fc; + --fg-muted: #9198a1; + --fg-faint: #767d84; + --border: #3d444d; + --border-strong: #5a6169; + --accent: #58a6ff; + --accent-fg: #0d1117; + --accent-soft: rgba(88, 166, 255, 0.16); + --accent-soft-fg: #58a6ff; + --danger: #ff7b72; + --danger-soft: rgba(255, 123, 114, 0.15); + --warn: #e3b341; + --warn-soft: rgba(227, 179, 65, 0.15); + --success: #3fb950; + --success-soft: rgba(63, 185, 80, 0.15); + --link: #79c0ff; + --unread-bg: #151b23; + --read-bg: #010409; + --selected-bg: rgba(88, 166, 255, 0.18); + --focus-ring: 0 0 0 3px rgba(88, 166, 255, 0.4); + --star: #d29922; + --q1: #79c0ff; + --q2: #56d364; + --q3: #d2a8ff; + --scrollbar: rgba(145, 152, 161, 0.35); + color-scheme: dark; + --shadow-1: 0 1px 2px rgba(0, 0, 0, 0.45); + --shadow-2: 0 8px 24px rgba(0, 0, 0, 0.55); + --shadow-3: 0 22px 60px -28px rgba(0, 0, 0, 0.75); +} + /* === generated palettes: end === */ /* Accent variants */ diff --git a/web/src/views/settings/AppearanceSettings.tsx b/web/src/views/settings/AppearanceSettings.tsx index 64f8ef6..a9cea41 100644 --- a/web/src/views/settings/AppearanceSettings.tsx +++ b/web/src/views/settings/AppearanceSettings.tsx @@ -33,6 +33,30 @@ const PALETTE_PREVIEW: Record = { light: "linear-gradient(135deg,#e6e7ed 0%,#d6d8df 55%,#2959aa 55%,#2959aa 78%,#8c4351 78%)", dark: "linear-gradient(135deg,#1a1b26 0%,#1f2130 55%,#7aa2f7 55%,#7aa2f7 78%,#bb9af7 78%)", }, + catppuccin: { + light: "linear-gradient(135deg,#e6e9ef 0%,#eff1f5 55%,#8839ef 55%,#8839ef 78%,#ea76cb 78%)", + dark: "linear-gradient(135deg,#1e1e2e 0%,#313244 55%,#cba6f7 55%,#cba6f7 78%,#f5c2e7 78%)", + }, + solarized: { + light: "linear-gradient(135deg,#fdf6e3 0%,#eee8d5 55%,#268bd2 55%,#268bd2 78%,#cb4b16 78%)", + dark: "linear-gradient(135deg,#002b36 0%,#073642 55%,#268bd2 55%,#268bd2 78%,#cb4b16 78%)", + }, + ayu: { + light: "linear-gradient(135deg,#f8f9fa 0%,#ebeef0 55%,#f29718 55%,#f29718 78%,#55b4d4 78%)", + dark: "linear-gradient(135deg,#0d1017 0%,#10141c 55%,#e6b450 55%,#e6b450 78%,#39bae6 78%)", + }, + kanagawa: { + light: "linear-gradient(135deg,#e5ddb0 0%,#f2ecbc 55%,#624c83 55%,#624c83 78%,#b35b79 78%)", + dark: "linear-gradient(135deg,#1f1f28 0%,#2a2a37 55%,#7e9cd8 55%,#7e9cd8 78%,#d27e99 78%)", + }, + everforest: { + light: "linear-gradient(135deg,#efebd4 0%,#fdf6e3 55%,#8da101 55%,#8da101 78%,#df69ba 78%)", + dark: "linear-gradient(135deg,#2d353b 0%,#343f44 55%,#a7c080 55%,#a7c080 78%,#d699b6 78%)", + }, + primer: { + light: "linear-gradient(135deg,#f6f8fa 0%,#ffffff 55%,#0969da 55%,#0969da 78%,#8250df 78%)", + dark: "linear-gradient(135deg,#0d1117 0%,#151b23 55%,#58a6ff 55%,#58a6ff 78%,#d2a8ff 78%)", + }, }; const MODES: Array<{ id: Mode; label: string }> = [