@stridge/noctis-theme-engine 1.0.0-beta.0

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.
@@ -0,0 +1,356 @@
1
+ import { adjust, clamp, mix, parseColor, toCss } from "../color/oklch.js";
2
+ import { getTextColor, isBright, solveOnBackground, solveTextColor } from "../color/contrast.js";
3
+ import { ACCENT_ACTIVE, ACCENT_HOVER, AI_SELECTION_ALPHA, BG_C_STEPS, BG_L_STEPS, BG_L_STEPS_LIGHT, BORDER_L_STEPS, BORDER_SELECTED_MIX, CONTROL, CONTROL_C_STEP, ELEVATION_C_STEP, ELEVATION_STEP, ELEVATION_STEP_LIGHT, ENGINE_VAR_PREFIX, FIELD_HOVER_L, FOCUS_MIN_CHROMA, GHOST_CONTROL_SOFTEN, HEADER_LIFT, MUTED_MIX, ON_FILL_TINT, OVERLAY_ALPHA, PRIMARY_DIM, SELECTED_HOVER, SELECTED_MIX, SELECTION_ALPHA, SIDEBAR_ITEM_LIFT, SIDEBAR_ITEM_MIX, STATUSES, STATUS_BORDER_EXTRA_MIX, STATUS_HOVER, STATUS_SEEDS, STATUS_TINT_FACTOR, SUNKEN_L_STEP, SUNKEN_L_STEP_LIGHT, TEXT_LC_TARGETS, THEME_VARS, WHITE_RESERVE } from "./tokens.js";
4
+ //#region src/generate/theme.ts
5
+ /**
6
+ * `generateTheme` — the seed → full token set core.
7
+ *
8
+ * A background seed walks contrast-scaled lightness steps to build the surface, border and
9
+ * control ladders; every text and status tier is APCA-solved for guaranteed legibility; and
10
+ * every emitted color is gamut-mapped to sRGB. Cross-surface elevation (menus, sidebar) is a
11
+ * full re-generation at a shifted base — see {@link generateScopeTheme}.
12
+ *
13
+ * Every primitive resolves through `override ?? computed`: an entry in
14
+ * {@link ThemeOverrides} replaces its primitive's computed value, and every dependent derives
15
+ * against the resolved value — an overridden `"bg-1"` is the canvas the text tiers, borders,
16
+ * controls, and status families solve against; an overridden `"accent"` feeds the accent set,
17
+ * focus ring, and selection mixes; an overridden part is embedded by its composites.
18
+ */
19
+ const BLACK = {
20
+ l: 0,
21
+ c: 0,
22
+ h: 0
23
+ };
24
+ const SCOPE_SEGMENTS = [
25
+ "el",
26
+ "su",
27
+ "mn"
28
+ ];
29
+ /** Engine primitive ids — the {@link THEME_VARS} names with the namespace prefix stripped. */
30
+ const PRIMITIVE_IDS = new Set(THEME_VARS.map((name) => name.slice(ENGINE_VAR_PREFIX.length)));
31
+ /** Ids whose values are CSS shorthand composites — override values pass through verbatim, unparsed. */
32
+ const COMPOSITE_IDS = new Set([
33
+ "shadow-border",
34
+ "focus-shadow",
35
+ "input-border"
36
+ ]);
37
+ /** Split a possibly scope-qualified override key (`"el-bg-3"`) into its segment and primitive id. */
38
+ function splitScope(key) {
39
+ for (const segment of SCOPE_SEGMENTS) if (key.startsWith(`${segment}-`)) return {
40
+ scope: segment,
41
+ id: key.slice(segment.length + 1)
42
+ };
43
+ return {
44
+ scope: null,
45
+ id: key
46
+ };
47
+ }
48
+ /**
49
+ * Whether a string can be embedded verbatim as a single CSS declaration value. A legitimate
50
+ * value never contains `{`, `}`, `<`, or a semicolon outside parentheses — any of those lets a
51
+ * value close its declaration block and smuggle whole rules into the string-built engine
52
+ * stylesheets (the runtime sheet and the SSR `<style>` text).
53
+ */
54
+ function isSafeCssValue(value) {
55
+ let depth = 0;
56
+ for (const char of value) {
57
+ if (char === "{" || char === "}" || char === "<") return false;
58
+ if (char === "(") depth += 1;
59
+ else if (char === ")") depth = Math.max(0, depth - 1);
60
+ else if (char === ";" && depth === 0) return false;
61
+ }
62
+ return true;
63
+ }
64
+ /**
65
+ * Reject unknown ids and invalid values up front, root and scope-qualified entries alike, so a
66
+ * typo fails loudly instead of silently generating a default theme. Color values must parse;
67
+ * composite values pass through verbatim, so they must be emission-safe ({@link isSafeCssValue})
68
+ * — a rule-delimiting character in a composite would escape the declaration in the emitted CSS.
69
+ */
70
+ function validateOverrides(overrides) {
71
+ for (const key of Object.keys(overrides)) {
72
+ const value = overrides[key];
73
+ if (value === void 0) continue;
74
+ const { id } = splitScope(key);
75
+ if (!PRIMITIVE_IDS.has(id)) throw new Error(`theme-engine: unknown theme override id "${key}"`);
76
+ if (!COMPOSITE_IDS.has(id)) parseColor(value);
77
+ else if (!isSafeCssValue(value)) throw new Error(`theme-engine: unsafe characters in theme override "${key}"`);
78
+ }
79
+ }
80
+ function resolveBright(bg, mode) {
81
+ if (mode === "light") return true;
82
+ if (mode === "dark") return false;
83
+ return isBright(bg);
84
+ }
85
+ /**
86
+ * Reserve a sliver of white headroom in bright mode: pull the canvas to at most `1 − WHITE_RESERVE`
87
+ * so elevated surfaces rise toward white without clamping. Dark canvases are returned unchanged.
88
+ */
89
+ function reserveCanvas(c, bright) {
90
+ return bright ? {
91
+ ...c,
92
+ l: Math.min(c.l, 1 - WHITE_RESERVE)
93
+ } : c;
94
+ }
95
+ /** On-fill text: auto white/black carrying at most a whisper of the surface's hue. */
96
+ function onFillText(surface) {
97
+ return {
98
+ l: getTextColor(surface).l,
99
+ c: Math.min(surface.c, ON_FILL_TINT),
100
+ h: surface.h
101
+ };
102
+ }
103
+ /** A translucent white (dark) / black (light) border matching the opaque border's L-distance. */
104
+ function alphaBorder(opaque, bg, bright) {
105
+ const alpha = bright ? clamp((bg.l - opaque.l) / Math.max(bg.l, .001), 0, 1) : clamp((opaque.l - bg.l) / Math.max(1 - bg.l, .001), 0, 1);
106
+ return {
107
+ l: bright ? 0 : 1,
108
+ c: 0,
109
+ h: 0,
110
+ a: alpha
111
+ };
112
+ }
113
+ /**
114
+ * Turn `{ background, accent, contrast, mode? }` into the flat L1 primitive token map.
115
+ * Pure and deterministic — no DOM, no globals.
116
+ *
117
+ * `options.overrides` entries replace their primitive's computed value (the override string is
118
+ * emitted verbatim) and feed every dependent derivation; scope-qualified entries are validated
119
+ * here but consumed only by the matching scope's run in {@link generateScopeTheme}.
120
+ */
121
+ function generateTheme(input, options) {
122
+ const overrides = options?.overrides ?? {};
123
+ validateOverrides(overrides);
124
+ const css = (id, computed) => overrides[id] ?? toCss(computed);
125
+ const composite = (id, computed) => overrides[id] ?? computed;
126
+ const overrideColor = (id) => {
127
+ const value = overrides[id];
128
+ return value === void 0 ? void 0 : parseColor(value);
129
+ };
130
+ const background = parseColor(input.background);
131
+ const accentSeed = parseColor(input.accent);
132
+ const accent = overrideColor("accent") ?? accentSeed;
133
+ const seedCanvas = overrideColor("bg-1") ?? background;
134
+ const bright = resolveBright(seedCanvas, input.mode);
135
+ const canvas = overrides["bg-1"] === void 0 ? reserveCanvas(seedCanvas, bright) : seedCanvas;
136
+ const contrast = Number.isFinite(input.contrast) ? clamp(input.contrast, 0, 100) : 0;
137
+ const direction = bright ? -1 : 1;
138
+ const gain = contrast / 30 * direction;
139
+ const magnitude = Math.abs(gain);
140
+ const controlGain = (bright ? -.8 : 1) * (contrast / 70);
141
+ const mutedAmount = bright ? MUTED_MIX.light : MUTED_MIX.dark;
142
+ const bg = (bright ? BG_L_STEPS_LIGHT : BG_L_STEPS).map((lStep, i) => adjust(canvas, {
143
+ l: lStep * magnitude,
144
+ c: (BG_C_STEPS[i] ?? 0) * magnitude
145
+ }));
146
+ const bgSunken = adjust(canvas, { l: -(bright ? SUNKEN_L_STEP_LIGHT : SUNKEN_L_STEP) * magnitude });
147
+ const bgSelected = overrideColor("bg-selected") ?? mix(canvas, accent, bright ? SELECTED_MIX.light : SELECTED_MIX.dark);
148
+ const bgSelectedHover = adjust(bgSelected, {
149
+ l: SELECTED_HOVER.l * direction,
150
+ c: SELECTED_HOVER.c * magnitude
151
+ });
152
+ const bgFocus = overrideColor("bg-focus") ?? adjust(mix(canvas, accent, .05), { l: .04 * gain });
153
+ const fg1 = overrideColor("fg-1") ?? getTextColor(canvas);
154
+ const fg2 = solveTextColor(canvas, TEXT_LC_TARGETS.fg2);
155
+ const fg3 = overrideColor("fg-3") ?? solveTextColor(canvas, TEXT_LC_TARGETS.fg3);
156
+ const fg4 = solveTextColor(canvas, TEXT_LC_TARGETS.fg4);
157
+ const link = solveOnBackground({
158
+ ...accent,
159
+ c: Math.max(accent.c, FOCUS_MIN_CHROMA)
160
+ }, canvas, 60);
161
+ const borderFaint = overrideColor("border-faint") ?? adjust(canvas, {
162
+ l: BORDER_L_STEPS.faint * gain,
163
+ c: .0026 * magnitude
164
+ });
165
+ const borderDefault = overrideColor("border-default") ?? adjust(canvas, {
166
+ l: BORDER_L_STEPS.default * gain,
167
+ c: .0026 * magnitude
168
+ });
169
+ const borderStrong = overrideColor("border-strong") ?? adjust(canvas, {
170
+ l: BORDER_L_STEPS.strong * gain,
171
+ c: .0026 * magnitude
172
+ });
173
+ const borderSelected = mix(canvas, accent, BORDER_SELECTED_MIX);
174
+ const accentHover = adjust(accent, {
175
+ l: ACCENT_HOVER.l * direction,
176
+ c: ACCENT_HOVER.c
177
+ });
178
+ const accentActive = adjust(accent, {
179
+ l: ACCENT_ACTIVE.l * direction,
180
+ c: ACCENT_ACTIVE.c
181
+ });
182
+ const accentMuted = mix(canvas, accent, mutedAmount * .7);
183
+ const primaryHover = adjust(fg1, { l: -PRIMARY_DIM.hover * direction });
184
+ const primaryActive = adjust(fg1, { l: -PRIMARY_DIM.active * direction });
185
+ const controlAt = (lStep, g) => adjust(canvas, {
186
+ l: lStep * g,
187
+ c: CONTROL_C_STEP * magnitude
188
+ });
189
+ const control = (lStep) => controlAt(lStep, controlGain);
190
+ const ghostGain = controlGain * (bright ? GHOST_CONTROL_SOFTEN : 1);
191
+ const ghost = (lStep) => controlAt(lStep, ghostGain);
192
+ const fieldHover = adjust(canvas, {
193
+ l: FIELD_HOVER_L * gain,
194
+ c: CONTROL_C_STEP * magnitude
195
+ });
196
+ const focusInBand = bright ? accent.l < .93 : accent.l > .45;
197
+ const focusRing = overrideColor("focus-ring") ?? (accent.c >= .06 && focusInBand ? accent : solveOnBackground({
198
+ ...accent,
199
+ c: Math.max(accent.c, .06)
200
+ }, canvas, 45));
201
+ const scrollbar = mix(canvas, fg1, .35);
202
+ const bg1Css = css("bg-1", bg[0]);
203
+ const bgFocusCss = css("bg-focus", bgFocus);
204
+ const fg1Css = css("fg-1", fg1);
205
+ const borderDefaultCss = css("border-default", borderDefault);
206
+ const borderStrongCss = css("border-strong", borderStrong);
207
+ const focusRingCss = css("focus-ring", focusRing);
208
+ const map = {
209
+ "--noctis-engine-bg-1": bg1Css,
210
+ "--noctis-engine-bg-2": css("bg-2", bg[1]),
211
+ "--noctis-engine-bg-3": css("bg-3", bg[2]),
212
+ "--noctis-engine-bg-4": css("bg-4", bg[3]),
213
+ "--noctis-engine-bg-5": css("bg-5", bg[4]),
214
+ "--noctis-engine-bg-6": css("bg-6", bg[5]),
215
+ "--noctis-engine-bg-sunken": css("bg-sunken", bgSunken),
216
+ "--noctis-engine-bg-selected": css("bg-selected", bgSelected),
217
+ "--noctis-engine-bg-selected-hover": css("bg-selected-hover", bgSelectedHover),
218
+ "--noctis-engine-bg-focus": bgFocusCss,
219
+ "--noctis-engine-fg-1": fg1Css,
220
+ "--noctis-engine-fg-2": css("fg-2", fg2),
221
+ "--noctis-engine-fg-3": css("fg-3", fg3),
222
+ "--noctis-engine-fg-4": css("fg-4", fg4),
223
+ "--noctis-engine-link": css("link", link),
224
+ "--noctis-engine-border-faint": css("border-faint", borderFaint),
225
+ "--noctis-engine-border-default": borderDefaultCss,
226
+ "--noctis-engine-border-strong": borderStrongCss,
227
+ "--noctis-engine-border-faint-alpha": css("border-faint-alpha", alphaBorder(borderFaint, canvas, bright)),
228
+ "--noctis-engine-border-alpha": css("border-alpha", alphaBorder(borderDefault, canvas, bright)),
229
+ "--noctis-engine-border-strong-alpha": css("border-strong-alpha", alphaBorder(borderStrong, canvas, bright)),
230
+ "--noctis-engine-border-selected": css("border-selected", borderSelected),
231
+ "--noctis-engine-divider": overrides["divider"] ?? borderStrongCss,
232
+ "--noctis-engine-accent": css("accent", accent),
233
+ "--noctis-engine-accent-fg": css("accent-fg", onFillText(accent)),
234
+ "--noctis-engine-accent-hover": css("accent-hover", accentHover),
235
+ "--noctis-engine-accent-active": css("accent-active", accentActive),
236
+ "--noctis-engine-accent-muted": css("accent-muted", accentMuted),
237
+ "--noctis-engine-primary-hover": css("primary-hover", primaryHover),
238
+ "--noctis-engine-primary-active": css("primary-active", primaryActive),
239
+ "--noctis-engine-control-2": css("control-2", control(CONTROL.secondary.base)),
240
+ "--noctis-engine-control-2-hover": css("control-2-hover", control(CONTROL.secondary.hover)),
241
+ "--noctis-engine-control-2-selected": css("control-2-selected", control(CONTROL.secondary.selected)),
242
+ "--noctis-engine-control-2-selected-hover": css("control-2-selected-hover", control(CONTROL.secondary.selectedHover)),
243
+ "--noctis-engine-control-3": css("control-3", ghost(CONTROL.tertiary.base)),
244
+ "--noctis-engine-control-3-hover": css("control-3-hover", ghost(CONTROL.tertiary.hover)),
245
+ "--noctis-engine-control-3-selected": css("control-3-selected", ghost(CONTROL.tertiary.selected)),
246
+ "--noctis-engine-control-3-selected-hover": css("control-3-selected-hover", ghost(CONTROL.tertiary.selectedHover)),
247
+ "--noctis-engine-control-fg": overrides["control-fg"] ?? fg1Css,
248
+ "--noctis-engine-focus-ring": focusRingCss,
249
+ "--noctis-engine-selection-bg": css("selection-bg", {
250
+ ...accent,
251
+ a: SELECTION_ALPHA
252
+ }),
253
+ "--noctis-engine-ai-selection-bg": css("ai-selection-bg", {
254
+ ...fg3,
255
+ a: AI_SELECTION_ALPHA
256
+ }),
257
+ "--noctis-engine-overlay": css("overlay", {
258
+ ...BLACK,
259
+ a: OVERLAY_ALPHA
260
+ }),
261
+ "--noctis-engine-shadow-color": css("shadow-color", BLACK),
262
+ "--noctis-engine-scrollbar": css("scrollbar", scrollbar),
263
+ "--noctis-engine-sidebar-item-bg": css("sidebar-item-bg", adjust(canvas, {
264
+ l: SIDEBAR_ITEM_LIFT * gain,
265
+ c: .002 * magnitude
266
+ })),
267
+ "--noctis-engine-sidebar-item-active": css("sidebar-item-active", mix(canvas, accent, SIDEBAR_ITEM_MIX)),
268
+ "--noctis-engine-header-bg": css("header-bg", adjust(canvas, {
269
+ l: HEADER_LIFT * gain,
270
+ c: .004 * magnitude
271
+ })),
272
+ "--noctis-engine-shadow-border": composite("shadow-border", `0 0 0 1px ${borderDefaultCss}`),
273
+ "--noctis-engine-focus-shadow": composite("focus-shadow", `0 0 0 1px ${focusRingCss}`),
274
+ "--noctis-engine-input-bg": overrides["input-bg"] ?? bg1Css,
275
+ "--noctis-engine-input-border": composite("input-border", `1px solid ${borderDefaultCss}`),
276
+ "--noctis-engine-field-hover": css("field-hover", fieldHover),
277
+ "--noctis-engine-field-focus": overrides["field-focus"] ?? bgFocusCss
278
+ };
279
+ for (const status of STATUSES) {
280
+ const color = overrideColor(status) ?? solveOnBackground(STATUS_SEEDS[status], canvas, 45);
281
+ const muted = overrideColor(`${status}-muted`) ?? mix(canvas, color, mutedAmount);
282
+ const border = mix(canvas, color, mutedAmount + STATUS_BORDER_EXTRA_MIX);
283
+ const tint = mix(canvas, color, mutedAmount * STATUS_TINT_FACTOR);
284
+ map[`${ENGINE_VAR_PREFIX}${status}`] = css(status, color);
285
+ map[`${ENGINE_VAR_PREFIX}${status}-hover`] = css(`${status}-hover`, adjust(color, {
286
+ l: STATUS_HOVER.l * direction,
287
+ c: STATUS_HOVER.c
288
+ }));
289
+ map[`${ENGINE_VAR_PREFIX}${status}-fg`] = css(`${status}-fg`, onFillText(color));
290
+ map[`${ENGINE_VAR_PREFIX}${status}-muted`] = css(`${status}-muted`, muted);
291
+ map[`${ENGINE_VAR_PREFIX}${status}-muted-fg`] = css(`${status}-muted-fg`, solveOnBackground(STATUS_SEEDS[status], muted, 45));
292
+ map[`${ENGINE_VAR_PREFIX}${status}-border`] = css(`${status}-border`, border);
293
+ map[`${ENGINE_VAR_PREFIX}${status}-tint`] = css(`${status}-tint`, tint);
294
+ }
295
+ return map;
296
+ }
297
+ /**
298
+ * The canvas for an elevation scope: the root canvas shifted toward the scope's elevation.
299
+ * Elevated/menu surfaces rise toward white (menu higher, reaching it in light mode) and sunken
300
+ * recedes — both directions hold in light and dark. The shift base is the reserved root canvas
301
+ * (or an explicit root `"bg-1"`), so a moved base re-derives every scope.
302
+ */
303
+ function scopeCanvas(input, level, rootOverride) {
304
+ const seed = rootOverride ?? parseColor(input.background);
305
+ const bright = resolveBright(seed, input.mode);
306
+ const base = rootOverride ?? reserveCanvas(seed, bright);
307
+ const step = bright ? ELEVATION_STEP_LIGHT : ELEVATION_STEP;
308
+ return adjust(base, {
309
+ l: level === "sunken" ? -step.sunken : step[level],
310
+ c: ELEVATION_C_STEP
311
+ });
312
+ }
313
+ const SCOPE_SEGMENT = {
314
+ elevated: "el",
315
+ menu: "mn",
316
+ sunken: "su"
317
+ };
318
+ /** The override entries qualified for `segment`, with the qualifier stripped off the keys. */
319
+ function scopeOverrides(overrides, segment) {
320
+ let scoped;
321
+ for (const key of Object.keys(overrides)) {
322
+ const value = overrides[key];
323
+ if (value === void 0 || !key.startsWith(`${segment}-`)) continue;
324
+ (scoped ??= {})[key.slice(segment.length + 1)] = value;
325
+ }
326
+ return scoped;
327
+ }
328
+ /**
329
+ * Generate the full token map for an elevation scope. `root` is the base theme; `elevated`
330
+ * (dialogs/sheets/popovers), `menu` (dropdowns/context menus/command palette — a higher float),
331
+ * and `sunken` (sidebar/wells) re-run the whole generator at a shifted base, so every role —
332
+ * surfaces, text, borders, status — stays internally consistent at that elevation. Apply the
333
+ * result to a scope element with {@link applyTheme}.
334
+ *
335
+ * Overrides scope precisely: a root `"bg-1"` entry moves the base every scope shifts from, a
336
+ * scope-qualified entry (`"el-bg-3"`) acts as that scope's own override map, and unqualified
337
+ * entries otherwise stay at the root — they never leak into a scope's run.
338
+ */
339
+ function generateScopeTheme(input, level, options) {
340
+ if (level === "root") return generateTheme(input, options);
341
+ const overrides = options?.overrides;
342
+ if (overrides) validateOverrides(overrides);
343
+ const scoped = overrides ? scopeOverrides(overrides, SCOPE_SEGMENT[level]) : void 0;
344
+ const bright = resolveBright(parseColor(overrides?.["bg-1"] ?? input.background), input.mode);
345
+ let bg1 = scoped?.["bg-1"];
346
+ if (bg1 === void 0) bg1 = toCss(scopeCanvas(input, level, overrides?.["bg-1"] === void 0 ? void 0 : parseColor(overrides["bg-1"])));
347
+ return generateTheme({
348
+ ...input,
349
+ mode: bright ? "light" : "dark"
350
+ }, { overrides: {
351
+ ...scoped,
352
+ "bg-1": bg1
353
+ } });
354
+ }
355
+ //#endregion
356
+ export { generateScopeTheme, generateTheme, isSafeCssValue };
@@ -0,0 +1,88 @@
1
+ //#region src/generate/tokens.d.ts
2
+ /**
3
+ * The engine's public input shape and its private L1 primitive token set.
4
+ *
5
+ * The set is deep: a neutral surface ramp with sunken + interaction-state surfaces, a
6
+ * weight × alpha border family, control surfaces, status ramps with seven sub-roles each,
7
+ * utility/state colors, and the border/focus/input composites. Elevation is a full
8
+ * re-generation at a shifted base (see `generateScopeTheme`), not extra tiers.
9
+ *
10
+ * The numeric constants are calibrated against a reference dark ramp converted into OKLCH.
11
+ * They are OKLCH offsets from the background seed, scaled by the contrast knob at runtime —
12
+ * a single parametric curve, not a lookup table.
13
+ */
14
+ /** Forced light/dark, or modeless (`auto` — background lightness decides). */
15
+ type ThemeMode = "auto" | "light" | "dark";
16
+ /** The three-input seed. `background`/`accent` accept any CSS color; `contrast` ∈ [0,100]. */
17
+ interface ThemeInput {
18
+ background: string;
19
+ accent: string;
20
+ contrast: number;
21
+ mode?: ThemeMode;
22
+ }
23
+ /**
24
+ * Generation-time primitive overrides, keyed by engine primitive id — the variable name without
25
+ * the `--noctis-engine-` prefix (`"bg-3"`, `"accent"`, `"border-default"`). Values are CSS color
26
+ * strings in any `parseColor`-accepted form; the shadow/input composites (`"shadow-border"`,
27
+ * `"input-border"`, …) take full composite strings verbatim. Scope-qualified ids (`"el-bg-3"`,
28
+ * `"su-…"`, `"mn-…"`) apply only inside that elevation scope's generation.
29
+ *
30
+ * Unlike a cascade override of the emitted variable, an override participates in derivation:
31
+ * every dependent primitive derives against the overridden value — text re-solves its APCA
32
+ * target on an overridden canvas, composites embed an overridden part, and the elevation scopes
33
+ * shift off an overridden `"bg-1"`. Unknown ids, unparseable color values, and composite values
34
+ * carrying rule-delimiting characters (`{`, `}`, `<`, a top-level `;`) throw.
35
+ *
36
+ * Gotcha: bracket-assigning a literal `"__proto__"` key (`overrides["__proto__"] = …`) is a
37
+ * silent no-op — JavaScript treats it as a prototype set, so the entry never becomes an own key
38
+ * and is never read. A JSON-sourced own `"__proto__"` key is rejected as an unknown id.
39
+ */
40
+ type ThemeOverrides = Readonly<Partial<Record<string, string>>>;
41
+ /**
42
+ * A surface's elevation scope — the vertical layer it sits on relative to the canvas. Each scope
43
+ * is a full re-generation of the theme at a base lightness shifted off the seed (see
44
+ * {@link ELEVATION_STEP}), so every role re-derives consistently for that layer. Ordered low → high:
45
+ *
46
+ * - `sunken` — recedes below the canvas. Sidebars, content wells, the scrollbar track, inset code
47
+ * blocks. The quietest layer; surfaces here read as carved into the page.
48
+ * - `root` — the canvas itself: the base page background and everything that sits directly on it.
49
+ * The default; most of the app lives here.
50
+ * - `elevated` — a raised surface that floats just above the canvas. Dialogs, sheets, drawers,
51
+ * popovers, cards lifted for emphasis — anything that should read as "on top of" the page.
52
+ * - `menu` — the highest float, lifted a touch more than `elevated`. Transient list overlays that
53
+ * open above everything else: dropdown and context menus, select listboxes, the command palette.
54
+ */
55
+ type ElevationLevel = "root" | "elevated" | "menu" | "sunken";
56
+ declare const STATUSES: readonly ["success", "warning", "danger", "info"];
57
+ /** Status family name. */
58
+ type StatusName = (typeof STATUSES)[number];
59
+ /**
60
+ * The reserved namespace prefix every engine primitive carries. The engine owns this stratum:
61
+ * no graph token of any tier ever serializes into a `--noctis-engine-*` name, so downstream
62
+ * lint can ban the whole namespace by prefix. Every primitive name and scope-set composition is
63
+ * derived from this single constant — there are no other literal copies of the prefix.
64
+ */
65
+ declare const ENGINE_VAR_PREFIX: "--noctis-engine-";
66
+ /**
67
+ * The cascade layer every engine emission lives in — the SSR emitters and the runtime sheet all
68
+ * declare into it, so an unlayered author rule on any engine variable outranks the engine by
69
+ * cascade. A cascade override replaces that one variable's value only; dependents do not
70
+ * re-derive (use {@link ThemeOverrides} for that).
71
+ */
72
+ declare const ENGINE_LAYER: "noctis.engine";
73
+ /** The flat set of generated L1 primitive CSS variables (names → CSS color/composite strings). */
74
+ declare const THEME_VARS: readonly ["--noctis-engine-bg-1", "--noctis-engine-bg-2", "--noctis-engine-bg-3", "--noctis-engine-bg-4", "--noctis-engine-bg-5", "--noctis-engine-bg-6", "--noctis-engine-bg-sunken", "--noctis-engine-bg-selected", "--noctis-engine-bg-selected-hover", "--noctis-engine-bg-focus", "--noctis-engine-fg-1", "--noctis-engine-fg-2", "--noctis-engine-fg-3", "--noctis-engine-fg-4", "--noctis-engine-link", "--noctis-engine-border-faint", "--noctis-engine-border-default", "--noctis-engine-border-strong", "--noctis-engine-border-faint-alpha", "--noctis-engine-border-alpha", "--noctis-engine-border-strong-alpha", "--noctis-engine-border-selected", "--noctis-engine-divider", "--noctis-engine-accent", "--noctis-engine-accent-fg", "--noctis-engine-accent-hover", "--noctis-engine-accent-active", "--noctis-engine-accent-muted", "--noctis-engine-primary-hover", "--noctis-engine-primary-active", "--noctis-engine-control-2", "--noctis-engine-control-2-hover", "--noctis-engine-control-2-selected", "--noctis-engine-control-2-selected-hover", "--noctis-engine-control-3", "--noctis-engine-control-3-hover", "--noctis-engine-control-3-selected", "--noctis-engine-control-3-selected-hover", "--noctis-engine-control-fg", "--noctis-engine-field-hover", "--noctis-engine-field-focus", ...("--noctis-engine-success" | "--noctis-engine-success-hover" | "--noctis-engine-success-fg" | "--noctis-engine-success-muted" | "--noctis-engine-success-muted-fg" | "--noctis-engine-success-border" | "--noctis-engine-success-tint" | "--noctis-engine-warning" | "--noctis-engine-warning-hover" | "--noctis-engine-warning-fg" | "--noctis-engine-warning-muted" | "--noctis-engine-warning-muted-fg" | "--noctis-engine-warning-border" | "--noctis-engine-warning-tint" | "--noctis-engine-danger" | "--noctis-engine-danger-hover" | "--noctis-engine-danger-fg" | "--noctis-engine-danger-muted" | "--noctis-engine-danger-muted-fg" | "--noctis-engine-danger-border" | "--noctis-engine-danger-tint" | "--noctis-engine-info" | "--noctis-engine-info-hover" | "--noctis-engine-info-fg" | "--noctis-engine-info-muted" | "--noctis-engine-info-muted-fg" | "--noctis-engine-info-border" | "--noctis-engine-info-tint")[], "--noctis-engine-focus-ring", "--noctis-engine-selection-bg", "--noctis-engine-ai-selection-bg", "--noctis-engine-overlay", "--noctis-engine-shadow-color", "--noctis-engine-scrollbar", "--noctis-engine-sidebar-item-bg", "--noctis-engine-sidebar-item-active", "--noctis-engine-header-bg", "--noctis-engine-shadow-border", "--noctis-engine-focus-shadow", "--noctis-engine-input-bg", "--noctis-engine-input-border"];
75
+ /** A generated primitive variable name. */
76
+ type ThemeVar = (typeof THEME_VARS)[number];
77
+ /**
78
+ * Compose the elevation-scope variant of an engine primitive: the scope segment (`el`/`su`/`mn`)
79
+ * is inserted *inside* the engine namespace, so `--noctis-engine-bg-1` at the `el` scope becomes
80
+ * `--noctis-engine-el-bg-1`. The single source of the scope-set spelling — the static `tokens.css`
81
+ * scope blocks, the SSR emitter, and the runtime writer all route through it so their names cannot
82
+ * drift apart.
83
+ */
84
+ declare function scopedEngineVar(key: ThemeVar, scope: "el" | "su" | "mn"): string;
85
+ /** A complete generated theme: every {@link ThemeVar} mapped to a CSS color/composite string. */
86
+ type ThemeMap = Record<ThemeVar, string>;
87
+ //#endregion
88
+ export { ENGINE_LAYER, ENGINE_VAR_PREFIX, ElevationLevel, StatusName, THEME_VARS, ThemeInput, ThemeMap, ThemeMode, ThemeOverrides, ThemeVar, scopedEngineVar };