@timmo001/groundwork-tokens 0.9.36 → 0.9.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/generated/Themes.qml +1 -1
- package/generated/opentui.ts +429 -0
- package/generated/themes.css +765 -0
- package/generated/themes.json +660 -0
- package/generated/themes.ts +660 -0
- package/package.json +4 -2
- package/src/desktop-themes.ts +253 -0
- package/src/generate.ts +2 -2
- package/src/page.ts +64 -6
- package/src/schema.ts +5 -1
- package/src/solve.ts +29 -20
- package/src/translate.ts +301 -0
- package/themes/github-dimmed.json +35 -0
- package/themes/github-high-contrast.json +35 -0
- package/themes/github.json +35 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timmo001/groundwork-tokens",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.38",
|
|
4
4
|
"description": "Groundwork's design tokens: the theme schema, the OKLCH solver and every theme as CSS and JSON",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
"./page": "./src/page.ts",
|
|
11
11
|
"./webmcp": "./src/webmcp.ts",
|
|
12
12
|
"./omarchy-current": "./src/omarchy-current.ts",
|
|
13
|
+
"./desktop-themes": "./src/desktop-themes.ts",
|
|
14
|
+
"./translate": "./src/translate.ts",
|
|
13
15
|
"./opentui": "./generated/opentui.ts",
|
|
14
16
|
"./themes.css": "./generated/themes.css",
|
|
15
17
|
"./themes.json": "./generated/themes.json",
|
|
@@ -53,7 +55,7 @@
|
|
|
53
55
|
"devDependencies": {
|
|
54
56
|
"@effect/platform-node": "4.1.2",
|
|
55
57
|
"@oxlint/plugins": "1.91.0",
|
|
56
|
-
"@timmo001/oxlint-rules": "0.6.
|
|
58
|
+
"@timmo001/oxlint-rules": "0.6.4",
|
|
57
59
|
"@types/bun": "1.4.4",
|
|
58
60
|
"oxlint": "1.91.0",
|
|
59
61
|
"oxlint-tsgolint": "7.0.2005",
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The color themes a machine has, translated into mapped themes so an app can
|
|
3
|
+
* offer them beside its own: every installed Omarchy theme and the theme
|
|
4
|
+
* each terminal (Ghostty, kitty, Alacritty and foot) uses. It needs a file system, so it's an entry
|
|
5
|
+
* of its own; drop the ones an app already offers with `distinctThemes`.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import type { Dirent } from "node:fs";
|
|
10
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
11
|
+
import { homedir } from "node:os";
|
|
12
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
13
|
+
|
|
14
|
+
import { omarchyTheme } from "./omarchy.ts";
|
|
15
|
+
import type { MappedTheme } from "./schema.ts";
|
|
16
|
+
import {
|
|
17
|
+
alacrittyPalette,
|
|
18
|
+
colorsTomlPalette,
|
|
19
|
+
distinctThemes,
|
|
20
|
+
footPalette,
|
|
21
|
+
ghosttyPalette,
|
|
22
|
+
kittyPalette,
|
|
23
|
+
translatePalette,
|
|
24
|
+
type TerminalPalette,
|
|
25
|
+
} from "./translate.ts";
|
|
26
|
+
|
|
27
|
+
export { distinctThemes, sameTheme, translatePalette } from "./translate.ts";
|
|
28
|
+
|
|
29
|
+
/** Where a desktop theme came from, such as `Omarchy` or `Ghostty`. */
|
|
30
|
+
export type DesktopThemeSource =
|
|
31
|
+
"Omarchy" | "Ghostty" | "kitty" | "Alacritty" | "foot";
|
|
32
|
+
|
|
33
|
+
const home = homedir();
|
|
34
|
+
|
|
35
|
+
const config = process.env.XDG_CONFIG_HOME || join(home, ".config");
|
|
36
|
+
const data = process.env.XDG_DATA_HOME || join(home, ".local", "share");
|
|
37
|
+
|
|
38
|
+
const expand = (path: string, from: string) => {
|
|
39
|
+
const unquoted = path
|
|
40
|
+
.trim()
|
|
41
|
+
.replace(/^\?/, "")
|
|
42
|
+
.replace(/^["']|["']$/g, "");
|
|
43
|
+
|
|
44
|
+
const tilde = unquoted.replace(/^~(?=\/|$)/, home);
|
|
45
|
+
|
|
46
|
+
return isAbsolute(tilde) ? tilde : resolve(from, tilde);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const text = (path: string) => readFile(path, "utf8").catch(() => undefined);
|
|
50
|
+
|
|
51
|
+
const folders = (path: string) =>
|
|
52
|
+
readdir(path, { withFileTypes: true }).catch((): Dirent[] => []);
|
|
53
|
+
|
|
54
|
+
const slug = (value: string) =>
|
|
55
|
+
value
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
58
|
+
.replace(/^-|-$/g, "");
|
|
59
|
+
|
|
60
|
+
// `tokyo-night` reads as `Tokyo Night`
|
|
61
|
+
const titled = (value: string) =>
|
|
62
|
+
value
|
|
63
|
+
.split(/[-_\s]+/)
|
|
64
|
+
.filter((word) => word !== "")
|
|
65
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
66
|
+
.join(" ");
|
|
67
|
+
|
|
68
|
+
const named = (source: DesktopThemeSource, name: string) => ({
|
|
69
|
+
id: `desktop-${slug(source)}-${slug(name)}`,
|
|
70
|
+
name: `${name} (${source})`,
|
|
71
|
+
family: source,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
/** Every Omarchy theme installed, the shipped ones and the user's own. */
|
|
75
|
+
const omarchyThemes = async () => {
|
|
76
|
+
const found: MappedTheme[] = [];
|
|
77
|
+
|
|
78
|
+
for (const root of [
|
|
79
|
+
join(data, "omarchy", "themes"),
|
|
80
|
+
join(config, "omarchy", "themes"),
|
|
81
|
+
]) {
|
|
82
|
+
for (const entry of await folders(root)) {
|
|
83
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
84
|
+
|
|
85
|
+
const folder = join(root, entry.name);
|
|
86
|
+
const theme = named("Omarchy", titled(entry.name));
|
|
87
|
+
const colors = await text(join(folder, "colors.toml"));
|
|
88
|
+
|
|
89
|
+
const parsed =
|
|
90
|
+
colors === undefined
|
|
91
|
+
? undefined
|
|
92
|
+
: omarchyTheme(colors, theme.name, theme.id);
|
|
93
|
+
|
|
94
|
+
if (parsed?._tag === "Success") {
|
|
95
|
+
found.push(parsed.success);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Older themes number their colors, or only have each app's file
|
|
100
|
+
for (const [file, palette] of [
|
|
101
|
+
["colors.toml", colorsTomlPalette],
|
|
102
|
+
["alacritty.toml", alacrittyPalette],
|
|
103
|
+
["ghostty.conf", ghosttyPalette],
|
|
104
|
+
["kitty.conf", kittyPalette],
|
|
105
|
+
] as const) {
|
|
106
|
+
const read = await text(join(folder, file));
|
|
107
|
+
|
|
108
|
+
const translated =
|
|
109
|
+
read === undefined
|
|
110
|
+
? undefined
|
|
111
|
+
: translatePalette(palette(read), theme);
|
|
112
|
+
|
|
113
|
+
if (translated !== undefined) {
|
|
114
|
+
found.push(translated);
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return found;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A terminal config with the files it includes, one level deep, joined in
|
|
126
|
+
* order so later colors win, as the terminal reads them.
|
|
127
|
+
*/
|
|
128
|
+
const withIncludes = async (
|
|
129
|
+
path: string,
|
|
130
|
+
include: RegExp,
|
|
131
|
+
extra: (text: string, from: string) => readonly string[] = () => [],
|
|
132
|
+
) => {
|
|
133
|
+
const main = await text(path);
|
|
134
|
+
|
|
135
|
+
if (main === undefined) return undefined;
|
|
136
|
+
|
|
137
|
+
const from = dirname(path);
|
|
138
|
+
const parts: string[] = [];
|
|
139
|
+
|
|
140
|
+
for (const line of main.split(/\r?\n/)) {
|
|
141
|
+
const match = include.exec(line);
|
|
142
|
+
|
|
143
|
+
if (match?.[1] !== undefined) {
|
|
144
|
+
for (const file of match[1].split(/,\s*/)) {
|
|
145
|
+
const included = await text(expand(file.replace(/[[\]]/g, ""), from));
|
|
146
|
+
|
|
147
|
+
if (included !== undefined) parts.push(included);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
for (const file of extra(main, from)) {
|
|
153
|
+
const included = await text(file);
|
|
154
|
+
|
|
155
|
+
if (included !== undefined) parts.push(included);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [...parts, main].join("\n");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/** Ghostty's theme: its config, the files it pulls in, and a named `theme`. */
|
|
162
|
+
const ghosttyConfig = () =>
|
|
163
|
+
withIncludes(
|
|
164
|
+
join(config, "ghostty", "config"),
|
|
165
|
+
/^\s*config-file\s*=\s*(.+)$/,
|
|
166
|
+
(main) => {
|
|
167
|
+
const theme = /^\s*theme\s*=\s*(.+)$/m.exec(main)?.[1]?.trim();
|
|
168
|
+
|
|
169
|
+
if (theme === undefined) return [];
|
|
170
|
+
|
|
171
|
+
// `light:A,dark:B` takes the dark one
|
|
172
|
+
const name =
|
|
173
|
+
/dark:([^,]+)/.exec(theme)?.[1]?.trim() ??
|
|
174
|
+
theme
|
|
175
|
+
.replace(/^light:/, "")
|
|
176
|
+
.split(",")[0]
|
|
177
|
+
?.trim() ??
|
|
178
|
+
theme;
|
|
179
|
+
|
|
180
|
+
return [
|
|
181
|
+
join(config, "ghostty", "themes", name),
|
|
182
|
+
join("/usr/share/ghostty/themes", name),
|
|
183
|
+
];
|
|
184
|
+
},
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const terminals: readonly {
|
|
188
|
+
readonly source: DesktopThemeSource;
|
|
189
|
+
readonly read: () => Promise<string | undefined>;
|
|
190
|
+
readonly palette: (text: string) => TerminalPalette;
|
|
191
|
+
}[] = [
|
|
192
|
+
{ source: "Ghostty", read: ghosttyConfig, palette: ghosttyPalette },
|
|
193
|
+
{
|
|
194
|
+
source: "kitty",
|
|
195
|
+
read: () =>
|
|
196
|
+
withIncludes(
|
|
197
|
+
join(config, "kitty", "kitty.conf"),
|
|
198
|
+
/^\s*(?:include|globinclude)\s+(.+)$/,
|
|
199
|
+
),
|
|
200
|
+
palette: kittyPalette,
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
source: "Alacritty",
|
|
204
|
+
read: () =>
|
|
205
|
+
withIncludes(
|
|
206
|
+
join(config, "alacritty", "alacritty.toml"),
|
|
207
|
+
/^\s*(?:general\.)?import\s*=\s*\[(.+)\]/,
|
|
208
|
+
),
|
|
209
|
+
palette: alacrittyPalette,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
source: "foot",
|
|
213
|
+
read: () =>
|
|
214
|
+
withIncludes(join(config, "foot", "foot.ini"), /^\s*include\s*=\s*(.+)$/),
|
|
215
|
+
palette: footPalette,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
|
|
219
|
+
/** The theme each terminal uses. */
|
|
220
|
+
const terminalThemes = async () => {
|
|
221
|
+
const found: MappedTheme[] = [];
|
|
222
|
+
|
|
223
|
+
for (const terminal of terminals) {
|
|
224
|
+
const read = await terminal.read();
|
|
225
|
+
|
|
226
|
+
if (read === undefined) continue;
|
|
227
|
+
|
|
228
|
+
const translated = translatePalette(
|
|
229
|
+
terminal.palette(read),
|
|
230
|
+
named(terminal.source, terminal.source),
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (translated !== undefined) found.push(translated);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return found;
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Every theme on the machine, translated: the terminals' first, as the ones
|
|
241
|
+
* in use, then Omarchy's. Themes that look the same are kept once; pass the
|
|
242
|
+
* app's own as `existing` to drop those too.
|
|
243
|
+
*/
|
|
244
|
+
export const readDesktopThemes = async (
|
|
245
|
+
existing: readonly Pick<MappedTheme, "name" | "palette">[] = [],
|
|
246
|
+
): Promise<readonly MappedTheme[]> => {
|
|
247
|
+
const [terminal, omarchy] = await Promise.all([
|
|
248
|
+
terminalThemes(),
|
|
249
|
+
omarchyThemes(),
|
|
250
|
+
]);
|
|
251
|
+
|
|
252
|
+
return distinctThemes([...terminal, ...omarchy], existing);
|
|
253
|
+
};
|
package/src/generate.ts
CHANGED
|
@@ -756,12 +756,12 @@ export const optionalPage = (themes: readonly ResolvedTheme[]) => {
|
|
|
756
756
|
return [
|
|
757
757
|
"---",
|
|
758
758
|
"title: Optional themes",
|
|
759
|
-
"description: Catppuccin
|
|
759
|
+
"description: Catppuccin, Tokyo Night and GitHub, mapped onto the roles so they stay faithful to their palettes. Apps opt in.",
|
|
760
760
|
"---",
|
|
761
761
|
"",
|
|
762
762
|
`{/* ${header} */}`,
|
|
763
763
|
"",
|
|
764
|
-
"
|
|
764
|
+
"Most flavors are one mode of their palette, and GitHub's cover both light and dark. Each is mapped straight onto the roles rather than solved, so it looks the way the palette's authors intended. They take Base's style. The [contrast report](/themes/contrast) shows where a palette sits below the core targets.",
|
|
765
765
|
"",
|
|
766
766
|
...families.flatMap((family) => [
|
|
767
767
|
`## ${family}`,
|
package/src/page.ts
CHANGED
|
@@ -11,10 +11,21 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import base from "../themes/base.json" with { type: "json" };
|
|
13
13
|
import bridge from "../themes/bridge.json" with { type: "json" };
|
|
14
|
+
import catppuccinFrappe from "../themes/catppuccin-frappe.json" with { type: "json" };
|
|
15
|
+
import catppuccinLatte from "../themes/catppuccin-latte.json" with { type: "json" };
|
|
16
|
+
import catppuccinMacchiato from "../themes/catppuccin-macchiato.json" with { type: "json" };
|
|
17
|
+
import catppuccinMocha from "../themes/catppuccin-mocha.json" with { type: "json" };
|
|
18
|
+
import githubDimmed from "../themes/github-dimmed.json" with { type: "json" };
|
|
19
|
+
import githubHighContrast from "../themes/github-high-contrast.json" with { type: "json" };
|
|
20
|
+
import github from "../themes/github.json" with { type: "json" };
|
|
14
21
|
import glazing from "../themes/glazing.json" with { type: "json" };
|
|
15
22
|
import slate from "../themes/slate.json" with { type: "json" };
|
|
16
23
|
import snag from "../themes/snag.json" with { type: "json" };
|
|
17
24
|
import survey from "../themes/survey.json" with { type: "json" };
|
|
25
|
+
import tokyoNightDay from "../themes/tokyo-night-day.json" with { type: "json" };
|
|
26
|
+
import tokyoNightMoon from "../themes/tokyo-night-moon.json" with { type: "json" };
|
|
27
|
+
import tokyoNightStorm from "../themes/tokyo-night-storm.json" with { type: "json" };
|
|
28
|
+
import tokyoNight from "../themes/tokyo-night.json" with { type: "json" };
|
|
18
29
|
import type { OmarchyCurrentTheme } from "./omarchy.ts";
|
|
19
30
|
import type { ThemeStore } from "./runtime.ts";
|
|
20
31
|
import type { Theme } from "./schema.ts";
|
|
@@ -71,6 +82,8 @@ export interface ThemeChoice {
|
|
|
71
82
|
readonly id: string;
|
|
72
83
|
readonly name: string;
|
|
73
84
|
readonly mode?: ThemeScheme | undefined;
|
|
85
|
+
/** The palette an optional theme comes from, such as `Catppuccin`. */
|
|
86
|
+
readonly family?: string | undefined;
|
|
74
87
|
}
|
|
75
88
|
|
|
76
89
|
/** Whether `choice` can show in `scheme`. */
|
|
@@ -87,6 +100,35 @@ export const coreThemeChoices: readonly ThemeChoice[] = [
|
|
|
87
100
|
snag,
|
|
88
101
|
].map(({ id, name }) => ({ id, name }));
|
|
89
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The optional themes, mapped from other palettes, by family and then from
|
|
105
|
+
* light to dark. One with a single mode is offered for that scheme alone;
|
|
106
|
+
* one with both, such as GitHub's, for either.
|
|
107
|
+
*/
|
|
108
|
+
export const optionalThemeChoices: readonly ThemeChoice[] = [
|
|
109
|
+
catppuccinLatte,
|
|
110
|
+
catppuccinFrappe,
|
|
111
|
+
catppuccinMacchiato,
|
|
112
|
+
catppuccinMocha,
|
|
113
|
+
tokyoNightDay,
|
|
114
|
+
tokyoNight,
|
|
115
|
+
tokyoNightStorm,
|
|
116
|
+
tokyoNightMoon,
|
|
117
|
+
github,
|
|
118
|
+
githubDimmed,
|
|
119
|
+
githubHighContrast,
|
|
120
|
+
].map((theme) => ({
|
|
121
|
+
id: theme.id,
|
|
122
|
+
name: theme.name,
|
|
123
|
+
family: theme.family,
|
|
124
|
+
mode:
|
|
125
|
+
"otherPalette" in theme
|
|
126
|
+
? undefined
|
|
127
|
+
: theme.mode === "light"
|
|
128
|
+
? "light"
|
|
129
|
+
: "dark",
|
|
130
|
+
}));
|
|
131
|
+
|
|
90
132
|
export interface PageThemeOptions {
|
|
91
133
|
/** The local storage key prefix, such as `my-site:theme`. */
|
|
92
134
|
readonly key: string;
|
|
@@ -101,6 +143,12 @@ export interface PageThemeOptions {
|
|
|
101
143
|
readonly defaultTheme: string;
|
|
102
144
|
/** The mode to start with. Defaults to `auto`. */
|
|
103
145
|
readonly defaultMode?: ThemeMode;
|
|
146
|
+
/**
|
|
147
|
+
* Whether the Omarchy theme takes over while the visitor hasn't picked
|
|
148
|
+
* one. With `false` it's only offered, and `defaultTheme` stays in use.
|
|
149
|
+
* Defaults to `true`.
|
|
150
|
+
*/
|
|
151
|
+
readonly followOmarchy?: boolean;
|
|
104
152
|
/** The element the attributes go on. Defaults to `<html>`. */
|
|
105
153
|
readonly root?: HTMLElement;
|
|
106
154
|
/**
|
|
@@ -178,21 +226,26 @@ const write = (key: string, value: string | null) => {
|
|
|
178
226
|
* defaults as `pageTheme`.
|
|
179
227
|
*/
|
|
180
228
|
export const themeBootScript = (
|
|
181
|
-
options: Pick<
|
|
229
|
+
options: Pick<
|
|
230
|
+
PageThemeOptions,
|
|
231
|
+
"key" | "defaultTheme" | "defaultMode" | "followOmarchy"
|
|
232
|
+
>,
|
|
182
233
|
) => {
|
|
183
234
|
const config = JSON.stringify({
|
|
184
235
|
keys: keys(options.key),
|
|
185
236
|
omarchy: omarchyKeys(options.key),
|
|
186
237
|
theme: options.defaultTheme,
|
|
187
238
|
mode: options.defaultMode ?? "auto",
|
|
239
|
+
follow: options.followOmarchy !== false,
|
|
188
240
|
custom: customThemeId,
|
|
189
241
|
style: customThemeStyleId,
|
|
190
242
|
omarchyId: omarchyThemeId,
|
|
191
243
|
omarchyStyle: omarchyThemeStyleId,
|
|
192
244
|
});
|
|
193
245
|
|
|
194
|
-
// With nothing picked, a cached Omarchy theme for the scheme is the
|
|
195
|
-
|
|
246
|
+
// With nothing picked, a cached Omarchy theme for the scheme is the
|
|
247
|
+
// default, unless it's only offered
|
|
248
|
+
return `(()=>{const c=${config};let s;try{s=localStorage}catch{}const g=(k)=>{try{return s?.getItem(k)??null}catch{return null}};const r=document.documentElement;const a=(id,css)=>{const e=document.createElement("style");e.id=id;e.textContent=css;document.head.append(e)};const m=g(c.keys.mode)??c.mode;const d=m==="dark"||m==="darker"||(m==="auto"&&matchMedia("(prefers-color-scheme: dark)").matches);const o=g(c.omarchy.css);let t=(d?g(c.keys.dark):null)??g(c.keys.theme)??(c.follow&&o&&g(c.omarchy.mode)===(d?"dark":"light")?c.omarchyId:c.theme);if(t===c.custom){const css=g(c.keys.css);if(css)a(c.style,css);else t=c.theme}else if(t===c.omarchyId){if(o)a(c.omarchyStyle,o);else t=c.theme}r.setAttribute("data-gw-theme",t);if(m==="auto")r.removeAttribute("data-gw-mode");else r.setAttribute("data-gw-mode",m)})();`;
|
|
196
249
|
};
|
|
197
250
|
|
|
198
251
|
/** Reads a theme from JSON text or an Omarchy `colors.toml`. */
|
|
@@ -284,9 +337,12 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
284
337
|
choices().some((theme) => theme.id === id && themeCovers(theme, scheme)));
|
|
285
338
|
|
|
286
339
|
// The Omarchy theme comes first where it covers the scheme, so the app
|
|
287
|
-
// matches the desktop until the visitor picks another
|
|
340
|
+
// matches the desktop until the visitor picks another, unless it's only
|
|
341
|
+
// offered
|
|
288
342
|
const fallback = (scheme: ThemeScheme) =>
|
|
289
|
-
|
|
343
|
+
options.followOmarchy !== false &&
|
|
344
|
+
omarchy !== null &&
|
|
345
|
+
themeCovers(omarchy, scheme)
|
|
290
346
|
? omarchy.id
|
|
291
347
|
: options.themes.some(
|
|
292
348
|
(theme) =>
|
|
@@ -437,7 +493,9 @@ export const pageTheme = (options: PageThemeOptions): PageTheme => {
|
|
|
437
493
|
read(stored.dark),
|
|
438
494
|
].includes(omarchyThemeId);
|
|
439
495
|
|
|
440
|
-
const unpicked =
|
|
496
|
+
const unpicked =
|
|
497
|
+
options.followOmarchy !== false && read(stored.theme) === null;
|
|
498
|
+
|
|
441
499
|
let mode: ThemeScheme | undefined;
|
|
442
500
|
|
|
443
501
|
if (theme !== null) {
|
package/src/schema.ts
CHANGED
|
@@ -108,15 +108,19 @@ export interface Palette extends Schema.Schema.Type<typeof Palette> {}
|
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* An optional theme mapped from an existing palette, such as Catppuccin, so it
|
|
111
|
-
* stays faithful.
|
|
111
|
+
* stays faithful. It takes Base's style. Most flavors are one mode; one that
|
|
112
|
+
* comes in both, such as GitHub's, also gives the other mode's palette.
|
|
112
113
|
*/
|
|
113
114
|
export const MappedTheme = Schema.Struct({
|
|
114
115
|
kind: Schema.tag("mapped"),
|
|
115
116
|
id: Schema.String,
|
|
116
117
|
name: Schema.String,
|
|
117
118
|
family: Schema.String,
|
|
119
|
+
/** The mode `palette` is for. */
|
|
118
120
|
mode: Schema.Literals(["light", "dark"]),
|
|
119
121
|
palette: Palette,
|
|
122
|
+
/** The other mode's palette, for a theme that covers both. */
|
|
123
|
+
otherPalette: Schema.optionalKey(Palette),
|
|
120
124
|
});
|
|
121
125
|
|
|
122
126
|
export interface MappedTheme extends Schema.Schema.Type<typeof MappedTheme> {}
|
package/src/solve.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
withLightness,
|
|
15
15
|
type Oklch,
|
|
16
16
|
} from "./color.ts";
|
|
17
|
-
import type {
|
|
17
|
+
import type { Palette, SolvedTheme, Style, Theme } from "./schema.ts";
|
|
18
18
|
|
|
19
19
|
export const statuses = [
|
|
20
20
|
"info",
|
|
@@ -261,18 +261,17 @@ const darker = (dark: Roles, by = 0.045) => {
|
|
|
261
261
|
};
|
|
262
262
|
};
|
|
263
263
|
|
|
264
|
-
const mappedRoles = (
|
|
264
|
+
const mappedRoles = (source: Palette, dark: boolean) => {
|
|
265
265
|
const palette = {
|
|
266
|
-
lower: fromHex(
|
|
267
|
-
background: fromHex(
|
|
268
|
-
raised: fromHex(
|
|
269
|
-
border: fromHex(
|
|
270
|
-
text: fromHex(
|
|
271
|
-
muted: fromHex(
|
|
272
|
-
accent: fromHex(
|
|
266
|
+
lower: fromHex(source.lower),
|
|
267
|
+
background: fromHex(source.background),
|
|
268
|
+
raised: fromHex(source.raised),
|
|
269
|
+
border: fromHex(source.border),
|
|
270
|
+
text: fromHex(source.text),
|
|
271
|
+
muted: fromHex(source.muted),
|
|
272
|
+
accent: fromHex(source.accent),
|
|
273
273
|
};
|
|
274
274
|
|
|
275
|
-
const dark = theme.mode === "dark";
|
|
276
275
|
const away = dark ? 1 : -1;
|
|
277
276
|
|
|
278
277
|
const surfaces = {
|
|
@@ -281,11 +280,11 @@ const mappedRoles = (theme: MappedTheme) => {
|
|
|
281
280
|
};
|
|
282
281
|
|
|
283
282
|
const seeds: Record<Status, Oklch> = {
|
|
284
|
-
info: fromHex(
|
|
285
|
-
success: fromHex(
|
|
286
|
-
important: fromHex(
|
|
287
|
-
warning: fromHex(
|
|
288
|
-
danger: fromHex(
|
|
283
|
+
info: fromHex(source.info),
|
|
284
|
+
success: fromHex(source.success),
|
|
285
|
+
important: fromHex(source.important),
|
|
286
|
+
warning: fromHex(source.warning),
|
|
287
|
+
danger: fromHex(source.danger),
|
|
289
288
|
};
|
|
290
289
|
|
|
291
290
|
return {
|
|
@@ -338,7 +337,20 @@ const mappedStyle = (base: Style): Style => ({
|
|
|
338
337
|
/** Resolve a theme into roles. Mapped themes take `baseStyle`, Base's style. */
|
|
339
338
|
export const resolve = (theme: Theme, baseStyle: Style): ResolvedTheme => {
|
|
340
339
|
if (theme.kind === "mapped") {
|
|
341
|
-
|
|
340
|
+
// `palette` is for its own mode, `otherPalette` for the opposite one
|
|
341
|
+
const light = theme.mode === "light" ? theme.palette : theme.otherPalette;
|
|
342
|
+
|
|
343
|
+
const dark = theme.mode === "dark" ? theme.palette : theme.otherPalette;
|
|
344
|
+
const modes: Partial<Record<Mode, Roles>> = {};
|
|
345
|
+
|
|
346
|
+
if (light !== undefined) modes.light = mappedRoles(light, false);
|
|
347
|
+
|
|
348
|
+
if (dark !== undefined) {
|
|
349
|
+
const roles = mappedRoles(dark, true);
|
|
350
|
+
|
|
351
|
+
modes.dark = roles;
|
|
352
|
+
modes.darker = darker(roles);
|
|
353
|
+
}
|
|
342
354
|
|
|
343
355
|
return {
|
|
344
356
|
id: theme.id,
|
|
@@ -348,10 +360,7 @@ export const resolve = (theme: Theme, baseStyle: Style): ResolvedTheme => {
|
|
|
348
360
|
family: theme.family,
|
|
349
361
|
examples: [],
|
|
350
362
|
style: mappedStyle(baseStyle),
|
|
351
|
-
modes
|
|
352
|
-
theme.mode === "dark"
|
|
353
|
-
? { dark: roles, darker: darker(roles) }
|
|
354
|
-
: { light: roles },
|
|
363
|
+
modes,
|
|
355
364
|
};
|
|
356
365
|
}
|
|
357
366
|
|