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

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 CHANGED
@@ -1,5 +1,7 @@
1
1
  # @stridge/noctis-theme-engine
2
2
 
3
+ ![license](https://img.shields.io/badge/license-MIT-blue) ![coverage](https://img.shields.io/badge/coverage-100%25-brightgreen)
4
+
3
5
  The framework-agnostic OKLCH theme generator at the base of the design system. From a
4
6
  `{ background, accent, contrast }` input it produces the complete private primitive ramp
5
7
  (`--noctis-engine-*`) — plus the elevation-scope re-derivations — as CSS variables, with
@@ -84,6 +84,7 @@ function mix(x, y, t) {
84
84
  /** Reduce chroma until the color sits inside the sRGB gamut, preserving L, H and alpha. */
85
85
  function toSrgbGamut(o) {
86
86
  const mapped = clampChroma(toCulori(o), "oklch", "rgb");
87
+ /* v8 ignore next -- defensive: clampChroma never returns undefined for a valid input */
87
88
  return mapped ? fromCulori(mapped, o.h) : o;
88
89
  }
89
90
  function round(value, places) {
@@ -100,6 +101,7 @@ function toCss(o) {
100
101
  const l = round(g.l, 4);
101
102
  const c = round(g.c, 4);
102
103
  const h = c === 0 ? 0 : round(g.h, 2);
104
+ /* v8 ignore next -- defensive: g.a is always set by toSrgbGamut/clampOklch */
103
105
  const a = g.a ?? 1;
104
106
  const base = `oklch(${l} ${c} ${h}`;
105
107
  return a < 1 ? `${base} / ${round(a, 3)})` : `${base})`;
@@ -139,10 +139,14 @@ function generateTheme(input, options) {
139
139
  const magnitude = Math.abs(gain);
140
140
  const controlGain = (bright ? -.8 : 1) * (contrast / 70);
141
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
- }));
142
+ const bg = (bright ? BG_L_STEPS_LIGHT : BG_L_STEPS).map((lStep, i) => {
143
+ /* v8 ignore next 2 -- defensive: BG_C_STEPS[i] is always defined for every bgSteps index */
144
+ const cStep = BG_C_STEPS[i] ?? 0;
145
+ return adjust(canvas, {
146
+ l: lStep * magnitude,
147
+ c: cStep * magnitude
148
+ });
149
+ });
146
150
  const bgSunken = adjust(canvas, { l: -(bright ? SUNKEN_L_STEP_LIGHT : SUNKEN_L_STEP) * magnitude });
147
151
  const bgSelected = overrideColor("bg-selected") ?? mix(canvas, accent, bright ? SELECTED_MIX.light : SELECTED_MIX.dark);
148
152
  const bgSelectedHover = adjust(bgSelected, {
package/dist/presets.d.ts CHANGED
@@ -15,8 +15,12 @@ interface ThemePreset extends ThemeInput {
15
15
  */
16
16
  declare const defaultPreset: ThemePreset;
17
17
  /**
18
- * The shipped presets. Dark is the Noctis default; Midnight is a deeper violet, Light the inverse,
19
- * Forest a deliberately distinct green. Seeds are authored in OKLCH so the calibration is exact.
18
+ * The presets Noctis itself ships the neutral core of the system, no chromatic identities. Dark is
19
+ * the default; Light is its inverse; the two High Contrast variants keep the same canvas and accent but
20
+ * push the contrast knob up for low-vision and bright-environment reading. Brand/showcase themes (a
21
+ * violet Midnight, a green Forest, a warm Sunset, …) are an application concern, not the package's — the
22
+ * engine generates any seed at runtime, so a consumer assembles its own palette and passes it to the
23
+ * provider's `presets`. Seeds are authored in OKLCH so the calibration is exact.
20
24
  */
21
25
  declare const presets: readonly ThemePreset[];
22
26
  //#endregion
package/dist/presets.js CHANGED
@@ -14,8 +14,12 @@ const defaultPreset = {
14
14
  contrast: 30
15
15
  };
16
16
  /**
17
- * The shipped presets. Dark is the Noctis default; Midnight is a deeper violet, Light the inverse,
18
- * Forest a deliberately distinct green. Seeds are authored in OKLCH so the calibration is exact.
17
+ * The presets Noctis itself ships the neutral core of the system, no chromatic identities. Dark is
18
+ * the default; Light is its inverse; the two High Contrast variants keep the same canvas and accent but
19
+ * push the contrast knob up for low-vision and bright-environment reading. Brand/showcase themes (a
20
+ * violet Midnight, a green Forest, a warm Sunset, …) are an application concern, not the package's — the
21
+ * engine generates any seed at runtime, so a consumer assembles its own palette and passes it to the
22
+ * provider's `presets`. Seeds are authored in OKLCH so the calibration is exact.
19
23
  */
20
24
  const presets = [
21
25
  defaultPreset,
@@ -26,16 +30,16 @@ const presets = [
26
30
  contrast: 30
27
31
  },
28
32
  {
29
- name: "Midnight",
30
- background: "oklch(0.18 0.03 268)",
31
- accent: "oklch(0.62 0.19 285)",
32
- contrast: 36
33
+ name: "Dark High Contrast",
34
+ background: "oklch(0.1711 0.0011 271)",
35
+ accent: "oklch(0.5674 0.1585 275)",
36
+ contrast: 70
33
37
  },
34
38
  {
35
- name: "Forest",
36
- background: "oklch(0.19 0.018 158)",
37
- accent: "oklch(0.65 0.15 152)",
38
- contrast: 32
39
+ name: "Light High Contrast",
40
+ background: "oklch(0.991 0.002 271)",
41
+ accent: "oklch(0.5674 0.1585 275)",
42
+ contrast: 70
39
43
  }
40
44
  ];
41
45
  //#endregion
@@ -1,4 +1,5 @@
1
1
  import { ThemeInput, ThemeOverrides } from "../generate/tokens.js";
2
+ import { ThemePreset } from "../presets.js";
2
3
  import { ReactNode } from "react";
3
4
 
4
5
  //#region src/react/provider.d.ts
@@ -11,6 +12,13 @@ interface ThemeProviderProps {
11
12
  * uncontrolled case. Overrides participate in derivation — see `ThemeOverrides`.
12
13
  */
13
14
  overrides?: ThemeOverrides;
15
+ /**
16
+ * The selectable presets exposed to the tree via `useTheme().presets` — the named seeds a theme
17
+ * picker offers. Defaults to the presets Noctis ships (Dark, Light, and their High Contrast
18
+ * variants); pass your own to advertise a custom palette. Purely the picker's menu — it does not
19
+ * change the active theme, which is driven by `initialInput`/`setTheme`.
20
+ */
21
+ presets?: readonly ThemePreset[];
14
22
  children: ReactNode;
15
23
  }
16
24
  /**
@@ -22,6 +30,7 @@ interface ThemeProviderProps {
22
30
  declare function ThemeProvider({
23
31
  initialInput,
24
32
  overrides: overridesProp,
33
+ presets,
25
34
  children
26
35
  }: ThemeProviderProps): ReactNode;
27
36
  //#endregion
@@ -13,7 +13,7 @@ import { jsx } from "react/jsx-runtime";
13
13
  * localStorage) so the next load's SSR seed and blocking script repaint correctly. Exposes
14
14
  * {@link useTheme}.
15
15
  */
16
- function ThemeProvider({ initialInput, overrides: overridesProp, children }) {
16
+ function ThemeProvider({ initialInput, overrides: overridesProp, presets: presets$1 = presets, children }) {
17
17
  const [input, setInput] = useState(() => initialInput ?? readPersistedInput() ?? defaultPreset);
18
18
  const [localOverrides, setLocalOverrides] = useState(() => initialInput ? void 0 : readPersistedOverrides() ?? void 0);
19
19
  const overrides = overridesProp ?? localOverrides;
@@ -37,12 +37,13 @@ function ThemeProvider({ initialInput, overrides: overridesProp, children }) {
37
37
  setTheme,
38
38
  overrides,
39
39
  setOverrides,
40
- presets
40
+ presets: presets$1
41
41
  }), [
42
42
  input,
43
43
  setTheme,
44
44
  overrides,
45
- setOverrides
45
+ setOverrides,
46
+ presets$1
46
47
  ]),
47
48
  children
48
49
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stridge/noctis-theme-engine",
3
- "version": "1.0.0-beta.0",
3
+ "version": "1.0.0-beta.3",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -47,6 +47,7 @@
47
47
  "tsdown": "0.21.10",
48
48
  "typescript": "6.0.3",
49
49
  "vitest": "4.1.8",
50
+ "@vitest/coverage-v8": "4.1.8",
50
51
  "@stridge/noctis-typescript": "0.0.0"
51
52
  },
52
53
  "scripts": {
@@ -54,6 +55,6 @@
54
55
  "check:publint": "publint",
55
56
  "check:publish": "pnpm run check:publint",
56
57
  "check:types": "tsc --noEmit",
57
- "test": "vitest --run"
58
+ "test": "vitest --run --coverage"
58
59
  }
59
60
  }