返回 DeepSeek-Reasonix
palettes.py
1 """Official theme palettes + WCAG contrast gate.
2
3 Defines the full token set for all eight official themes and checks the same
4 pairs the Go backend validates (fg/bg, fg/chat >= 4.5; fgFaint/bg >= 3.0;
5 accentFg/accent >= 3.0). ok/warn/err are intentionally unset so they inherit
6 the base style. `write_manifests()` emits theme.json files.
7 """
8 from __future__ import annotations
9
10 import json
11 import os
12 import shutil
13 import sys
14
15 HERE = os.path.dirname(__file__)
16 OUT = os.path.join(HERE, "out")
17 DEST = os.path.join(HERE, "..", "..", "desktop", "themes", "official")
18
19 SURFACE_KEYS = ["bg", "bgSoft", "bgElev", "panel", "sidebar", "chat", "workspace", "workspaceFiles"]
20 TEXT_KEYS = ["border", "borderSoft", "fg", "fgDim", "fgFaint", "accent", "accentFg"]
21
22 # focusX, focusY / homeOpacity / taskOpacity / overlayStrength from the plan table.
23 THEMES = {
24 "official-rose-dawn": {
25 "name": "Rose Dawn",
26 "zh": "玫瑰晨光",
27 "baseStyle": "graphite",
28 "corners": "round",
29 "bgparams": (0.72, 0.43, 1.0, 0.20, 0.68),
30 "description": "Ivory dawn light, soft roses and an original illustrated muse on the right.",
31 "light": {
32 "bg": "#FFF7F8", "bgSoft": "#FBEFF1", "bgElev": "#FFFFFF", "panel": "#FFFFFF",
33 "sidebar": "#FBECEF", "chat": "#FFFCFC", "workspace": "#FFFFFF", "workspaceFiles": "#FDF3F4",
34 "border": "#E2BCC6", "borderSoft": "#F5E3E7",
35 "fg": "#3A252C", "fgDim": "#6D4A55", "fgFaint": "#A97B87",
36 "accent": "#B43F65", "accentFg": "#FFF7F8",
37 },
38 "dark": {
39 "bg": "#1E1419", "bgSoft": "#281B21", "bgElev": "#312329", "panel": "#2A1D24",
40 "sidebar": "#251922", "chat": "#231820", "workspace": "#312329", "workspaceFiles": "#281C22",
41 "border": "#4C3440", "borderSoft": "#3A2832",
42 "fg": "#FFF3F6", "fgDim": "#D9B3C0", "fgFaint": "#A87C8C",
43 "accent": "#E26D91", "accentFg": "#2A121D",
44 },
45 },
46 "official-fortune-forge": {
47 "name": "Fortune Forge",
48 "zh": "鸿运工坊",
49 "baseStyle": "amber",
50 "corners": "soft",
51 "bgparams": (0.74, 0.44, 1.0, 0.20, 0.70),
52 "description": "Warm ivory workshop with vermilion, gold and jade around an original lucky programmer.",
53 "light": {
54 "bg": "#FFF8E8", "bgSoft": "#F9EFD5", "bgElev": "#FFFDF6", "panel": "#FFFDF6",
55 "sidebar": "#F8EDD3", "chat": "#FFFBF1", "workspace": "#FFFDF6", "workspaceFiles": "#FAF1DC",
56 "border": "#D6B87E", "borderSoft": "#F2E7CD",
57 "fg": "#382116", "fgDim": "#6E4E35", "fgFaint": "#A98A68",
58 "accent": "#A92D22", "accentFg": "#FFF8E8",
59 },
60 "dark": {
61 "bg": "#1D140D", "bgSoft": "#271C12", "bgElev": "#302417", "panel": "#291E13",
62 "sidebar": "#251B11", "chat": "#231A11", "workspace": "#302417", "workspaceFiles": "#271D13",
63 "border": "#4D3A22", "borderSoft": "#3B2C1A",
64 "fg": "#FFF2D1", "fgDim": "#DDC49C", "fgFaint": "#A9926B",
65 "accent": "#E8AD38", "accentFg": "#241606",
66 },
67 },
68 "official-crimson-horizon": {
69 "name": "Crimson Horizon",
70 "zh": "赤曜新城",
71 "baseStyle": "graphite",
72 "corners": "soft",
73 "bgparams": (0.75, 0.45, 0.98, 0.22, 0.66),
74 "description": "Pearl-white sky over a coral-red future city skyline. No people, pure skyline.",
75 "light": {
76 "bg": "#FFF8F7", "bgSoft": "#F9EEEC", "bgElev": "#FFFFFF", "panel": "#FFFFFF",
77 "sidebar": "#F9ECEA", "chat": "#FFFBFA", "workspace": "#FFFFFF", "workspaceFiles": "#FBF1EF",
78 "border": "#DFB3AD", "borderSoft": "#F6E4E2",
79 "fg": "#301D1D", "fgDim": "#6B4644", "fgFaint": "#A9807D",
80 "accent": "#B92B38", "accentFg": "#FFF8F7",
81 },
82 "dark": {
83 "bg": "#190D11", "bgSoft": "#221318", "bgElev": "#2B181E", "panel": "#25141A",
84 "sidebar": "#211218", "chat": "#201116", "workspace": "#2B181E", "workspaceFiles": "#221319",
85 "border": "#582F3C", "borderSoft": "#361B24",
86 "fg": "#FFF1F2", "fgDim": "#DFB3B6", "fgFaint": "#AC7E84",
87 "accent": "#FF6772", "accentFg": "#2A0E12",
88 },
89 },
90 "official-sage-breeze": {
91 "name": "Sage Breeze",
92 "zh": "鼠尾草清风",
93 "baseStyle": "slate",
94 "corners": "soft",
95 "bgparams": (0.73, 0.44, 1.0, 0.20, 0.68),
96 "description": "Cream paper, sage sprigs and an original illustrated reader on the right.",
97 "light": {
98 "bg": "#F7F7EF", "bgSoft": "#EFEFE2", "bgElev": "#FCFCF6", "panel": "#FCFCF6",
99 "sidebar": "#EEEEE0", "chat": "#FAFAF4", "workspace": "#FCFCF6", "workspaceFiles": "#F1F1E5",
100 "border": "#BFC095", "borderSoft": "#E8E8D8",
101 "fg": "#26332D", "fgDim": "#4E6157", "fgFaint": "#7C8F83",
102 "accent": "#47735F", "accentFg": "#F7F7EF",
103 },
104 "dark": {
105 "bg": "#101814", "bgSoft": "#17211C", "bgElev": "#1E2922", "panel": "#19231D",
106 "sidebar": "#161F1A", "chat": "#151E19", "workspace": "#1E2922", "workspaceFiles": "#17211C",
107 "border": "#32443A", "borderSoft": "#25322B",
108 "fg": "#EEF6F0", "fgDim": "#B7CDBF", "fgFaint": "#7E968A",
109 "accent": "#84CBA7", "accentFg": "#0E1A13",
110 },
111 },
112 "official-spark-notebook": {
113 "name": "Spark Notebook",
114 "zh": "灵感手账",
115 "baseStyle": "aurora",
116 "corners": "round",
117 "bgparams": (0.74, 0.46, 0.98, 0.20, 0.68),
118 "description": "Off-white notebook grid with teal, yellow and coral stationery around an original anime adult.",
119 "light": {
120 "bg": "#FFF9ED", "bgSoft": "#F9F1DE", "bgElev": "#FFFDF7", "panel": "#FFFDF7",
121 "sidebar": "#F8F0DC", "chat": "#FFFCF4", "workspace": "#FFFDF7", "workspaceFiles": "#FAF3E2",
122 "border": "#D2BC8E", "borderSoft": "#F2E9D4",
123 "fg": "#2B2F35", "fgDim": "#565D66", "fgFaint": "#848D98",
124 "accent": "#007B78", "accentFg": "#F3FBFA",
125 },
126 "dark": {
127 "bg": "#14171A", "bgSoft": "#1B1F23", "bgElev": "#23282D", "panel": "#1D2226",
128 "sidebar": "#1A1E22", "chat": "#191D21", "workspace": "#23282D", "workspaceFiles": "#1B2024",
129 "border": "#38404A", "borderSoft": "#2A3138",
130 "fg": "#F8F5E9", "fgDim": "#C9CCBF", "fgFaint": "#8B918A",
131 "accent": "#42D1C6", "accentFg": "#08201E",
132 },
133 },
134 "official-violet-starlight": {
135 "name": "Violet Starlight",
136 "zh": "紫曜星夜",
137 "baseStyle": "nocturne",
138 "corners": "round",
139 "bgparams": (0.73, 0.44, 0.96, 0.18, 0.72),
140 "description": "Blue-violet starfield with glowing butterflies and an original silhouette muse.",
141 "light": {
142 "bg": "#F7F4FF", "bgSoft": "#EFEBFB", "bgElev": "#FCFAFF", "panel": "#FCFAFF",
143 "sidebar": "#EEE9FA", "chat": "#FAF8FF", "workspace": "#FCFAFF", "workspaceFiles": "#F1EDFB",
144 "border": "#CABDEC", "borderSoft": "#E8E3F7",
145 "fg": "#251F3C", "fgDim": "#544B74", "fgFaint": "#9188B3",
146 "accent": "#6242C7", "accentFg": "#F7F4FF",
147 },
148 "dark": {
149 "bg": "#0C1022", "bgSoft": "#131834", "bgElev": "#1A2140", "panel": "#151B36",
150 "sidebar": "#121732", "chat": "#111630", "workspace": "#1A2140", "workspaceFiles": "#131834",
151 "border": "#2E3A66", "borderSoft": "#222B4E",
152 "fg": "#F4F2FF", "fgDim": "#C2BDE8", "fgFaint": "#8580B8",
153 "accent": "#9B86FF", "accentFg": "#14102E",
154 },
155 },
156 "official-cyan-stage": {
157 "name": "Cyan Stage",
158 "zh": "青岚舞台",
159 "baseStyle": "carbon",
160 "corners": "round",
161 "bgparams": (0.74, 0.45, 0.96, 0.18, 0.72),
162 "description": "Airy cyan future stage with light rings and an original digital performer.",
163 "light": {
164 "bg": "#F1FCFD", "bgSoft": "#E4F5F7", "bgElev": "#FAFEFE", "panel": "#FAFEFE",
165 "sidebar": "#E3F4F6", "chat": "#F6FDFE", "workspace": "#FAFEFE", "workspaceFiles": "#E8F6F8",
166 "border": "#9CCFD7", "borderSoft": "#DAEDF0",
167 "fg": "#173238", "fgDim": "#43606A", "fgFaint": "#71939D",
168 "accent": "#007C92", "accentFg": "#F1FCFD",
169 },
170 "dark": {
171 "bg": "#07181D", "bgSoft": "#0C2229", "bgElev": "#112C34", "panel": "#0E252D",
172 "sidebar": "#0B2128", "chat": "#0A2027", "workspace": "#112C34", "workspaceFiles": "#0C232A",
173 "border": "#1F4550", "borderSoft": "#16333C",
174 "fg": "#E9FCFF", "fgDim": "#AEDBE2", "fgFaint": "#6FA5AF",
175 "accent": "#37D7E4", "accentFg": "#04222a",
176 },
177 },
178 "official-noir-gold": {
179 "name": "Noir Gold",
180 "zh": "黑金序曲",
181 "baseStyle": "carbon",
182 "corners": "soft",
183 "bgparams": (0.73, 0.43, 0.94, 0.18, 0.74),
184 "description": "Champagne field, black velvet curtain and gold spotlights around an original gentleman.",
185 "light": {
186 "bg": "#FCF8EE", "bgSoft": "#F6F0DF", "bgElev": "#FEFBF4", "panel": "#FEFBF4",
187 "sidebar": "#F5EFDD", "chat": "#FDFAF2", "workspace": "#FEFBF4", "workspaceFiles": "#F7F1E1",
188 "border": "#CCBE94", "borderSoft": "#EFE8D2",
189 "fg": "#2A241B", "fgDim": "#5C5340", "fgFaint": "#9A8F74",
190 "accent": "#7A5A16", "accentFg": "#FCF8EE",
191 },
192 "dark": {
193 "bg": "#0D0B09", "bgSoft": "#15120E", "bgElev": "#1D1913", "panel": "#171410",
194 "sidebar": "#14110D", "chat": "#131009", "workspace": "#1D1913", "workspaceFiles": "#15120E",
195 "border": "#463B27", "borderSoft": "#2A2418",
196 "fg": "#F8F1DF", "fgDim": "#D6CBAE", "fgFaint": "#968C6E",
197 "accent": "#D9B45B", "accentFg": "#1D1503",
198 },
199 },
200 }
201
202
203 def _lum(hexs: str) -> float:
204 s = hexs.lstrip("#")[:6]
205 rgb = [int(s[i : i + 2], 16) / 255 for i in (0, 2, 4)]
206
207 def lin(c):
208 return c / 12.92 if c <= 0.04045 else ((c + 0.055) / 1.055) ** 2.4
209
210 return 0.2126 * lin(rgb[0]) + 0.7152 * lin(rgb[1]) + 0.0722 * lin(rgb[2])
211
212
213 def ratio(a: str, b: str) -> float:
214 la, lb = _lum(a), _lum(b)
215 return (max(la, lb) + 0.05) / (min(la, lb) + 0.05)
216
217
218 def check() -> int:
219 fails = 0
220 for tid, spec in THEMES.items():
221 for mode in ("light", "dark"):
222 tk = spec[mode]
223 pairs = [
224 ("fg/bg", tk["fg"], tk["bg"], 4.5),
225 ("fg/chat", tk["fg"], tk["chat"], 4.5),
226 ("fg/panel", tk["fg"], tk["panel"], 4.5),
227 ("fgDim/bg", tk["fgDim"], tk["bg"], 3.0),
228 ("fgFaint/bg", tk["fgFaint"], tk["bg"], 3.0),
229 ("fgFaint/panel", tk["fgFaint"], tk["panel"], 3.0),
230 ("accentFg/accent", tk["accentFg"], tk["accent"], 3.0),
231 ("accent/bg", tk["accent"], tk["bg"], 3.0),
232 ("border/bg", tk["border"], tk["bg"], 1.6),
233 ]
234 for name, a, b, minimum in pairs:
235 r = ratio(a, b)
236 if r < minimum:
237 fails += 1
238 print(f"FAIL {tid} {mode} {name}: {r:.2f} < {minimum} ({a} on {b})")
239 print("contrast gate:", "OK — zero warnings" if fails == 0 else f"{fails} failures")
240 return fails
241
242
243 def write_manifests():
244 for tid, spec in THEMES.items():
245 fx, fy, home, task, overlay = spec["bgparams"]
246 manifest = {
247 "schemaVersion": 1,
248 "id": tid,
249 "name": spec["name"],
250 "author": "Reasonix Contributors",
251 "description": spec["description"],
252 "license": "MIT",
253 "baseStyle": spec["baseStyle"],
254 "tokens": {"light": spec["light"], "dark": spec["dark"]},
255 "recipes": {"density": "comfortable", "corners": spec["corners"]},
256 "background": {
257 "image": "background.webp",
258 "focusX": fx,
259 "focusY": fy,
260 "safeArea": "left",
261 "homeOpacity": home,
262 "taskOpacity": task,
263 "overlayStrength": overlay,
264 },
265 }
266 tdir = os.path.join(DEST, tid)
267 os.makedirs(tdir, exist_ok=True)
268 with open(os.path.join(tdir, "theme.json"), "w") as f:
269 json.dump(manifest, f, indent=2, ensure_ascii=False)
270 f.write("\n")
271 for name in ("background.webp", "preview.webp"):
272 src = os.path.join(OUT, tid, name)
273 shutil.copyfile(src, os.path.join(tdir, name))
274 print(f"wrote {tdir}")
275
276
277 if __name__ == "__main__":
278 bad = check()
279 if bad:
280 sys.exit(1)
281 if "--write" in sys.argv:
282 write_manifests()
283
283 lines PYTHON