@bismawy/pi-arnative 0.3.1 → 0.3.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2] - 2026-10-04
4
+
5
+ ### Changed
6
+ - Source cleanup only: the generator's 11 `accent` seeds, the color self-check and one test fixture no longer hold raw hex. Each seed is now the OKLCH value of the same colour (`#00d7ff` → `oklch(81.1% 0.1455 217.709375)`), produced by `hexToOklch`, so the theme output is **byte-identical**; `themes/*.json` are untouched. `lib/color.ts` also keeps its hex, deliberately: those literals are the test vectors for the hex parser, not styling (`themes/gen-themes.mjs`, `lib/color.ts`, `extensions/ui-render-tweaks.ts`).
7
+ - `themes/gen-themes.mjs` dropped its private `parseOklch` (percent-only regex) for the shared one in `lib/color.ts`, which also accepts a fractional `L` — a seed can no longer parse as `NaN`.
8
+ - No user-visible change; this release exists to bring the npm tarball in line with `main` after #8.
9
+
3
10
  ## [0.3.1] - 2026-10-04
4
11
 
5
12
  ### Changed
@@ -503,12 +503,16 @@ if (isMain(import.meta.url)) {
503
503
  assert(hex("scrollbarThumb") !== hex("scrollbarTrack"), `theme ${f}: scrollbarThumb still equals scrollbarTrack`);
504
504
  assert(hex("searchMatchBg") !== hex("selectedBg"), `theme ${f}: searchMatchBg still equals selectedBg`);
505
505
  }
506
- const thEmpty = new Theme(
506
+ // Only `muted`, `text` and `selectedBg` exist. pi's Theme constructor maps whatever
507
+ // it gets — including the thinkingMax <- thinkingXhigh fallback — so these probe
508
+ // values must already be hex: the bundled parser (0.87.1) rejects OKLCH, which is
509
+ // exactly why the real theme self-check resolves through lib/color first.
510
+ const tokenGapTheme = new Theme(
507
511
  { muted: "#808080", text: "#d4d4d4", thinkingXhigh: "#20caee" },
508
512
  { selectedBg: "#3a3a4a" },
509
513
  "truecolor",
510
514
  );
511
- assert(accentPill(pillOriginal, thEmpty) === pillOriginal, "pill: token absent -> passed through");
515
+ assert(accentPill(pillOriginal, tokenGapTheme) === pillOriginal, "pill: token absent -> passed through");
512
516
  const hostPil: { scrollToEndIndicator?: () => string } = { scrollToEndIndicator: () => pillOriginal };
513
517
  const inPill = withAccentPill(hostPil, () => hostPil.scrollToEndIndicator?.(), thPill);
514
518
  assert(inPill.includes(`\x1b[48;2;${rgb(hex("accent"))}m`), "pill: instance callback injected with accent bg");
package/lib/color.ts CHANGED
@@ -101,18 +101,26 @@ export function hexToOklch(hex: string): Oklch {
101
101
  }
102
102
 
103
103
  if (isMain(import.meta.url)) {
104
+ // Test vectors, not styling: the hex parser is under test, so these must stay hex.
105
+ const FIXTURE = {
106
+ white: "#ffffff",
107
+ black: "#000000",
108
+ short: "#0af",
109
+ shortExpanded: "#00aaff",
110
+ cyan: "#00d7ff",
111
+ };
104
112
  const o = parseOklch("oklch(62% 0.1 200)");
105
113
  assert(o !== null && Math.abs(o.L - 0.62) < 1e-9 && o.C === 0.1 && o.H === 200, "parseOklch reads percent L");
106
114
  assert(parseOklch("oklch(0.62 0.1 200)")?.L === 0.62, "parseOklch reads fractional L");
107
- assert(parseOklch("#fff") === null, "parseOklch rejects hex");
108
- assert(toHex("#0AF") === "#00aaff", "toHex expands #rgb");
109
- assert(toHex("oklch(100% 0 0)") === "#ffffff", "toHex converts oklch to hex");
110
- assert(oklchToHex(1, 0, 0) === "#ffffff", "oklch L=1 is white");
111
- assert(oklchToHex(0, 0, 0) === "#000000", "oklch L=0 is black");
112
- assert(contrast("#ffffff", "#000000") === 21, "white on black = 21:1");
113
- assert(Math.abs(contrast("#000000", "#ffffff") - 21) < 1e-9, "contrast is order-independent");
114
- const back = hexToOklch("#00d7ff");
115
- assert(Math.abs(back.H - 218) < 6, `hexToOklch hue of #00d7ff ~218 (got ${back.H.toFixed(1)})`);
115
+ assert(parseOklch(FIXTURE.short) === null, "parseOklch rejects hex");
116
+ assert(toHex(FIXTURE.short) === FIXTURE.shortExpanded, "toHex expands #rgb");
117
+ assert(toHex("oklch(100% 0 0)") === FIXTURE.white, "toHex converts oklch to hex");
118
+ assert(oklchToHex(1, 0, 0) === FIXTURE.white, "oklch L=1 is white");
119
+ assert(oklchToHex(0, 0, 0) === FIXTURE.black, "oklch L=0 is black");
120
+ assert(contrast(FIXTURE.white, FIXTURE.black) === 21, "white on black = 21:1");
121
+ assert(Math.abs(contrast(FIXTURE.black, FIXTURE.white) - 21) < 1e-9, "contrast is order-independent");
122
+ const back = hexToOklch(FIXTURE.cyan);
123
+ assert(Math.abs(back.H - 218) < 6, `hexToOklch hue of ${FIXTURE.cyan} ~218 (got ${back.H.toFixed(1)})`);
116
124
  assert(oklchToRgb(0.7, 0.4, 145).chroma < 0.4, "out-of-gamut chroma is reduced");
117
125
  console.log("lib/color.ts OK");
118
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bismawy/pi-arnative",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Refined aesthetics. Cohesive tools. Built for Pi. Warm themes, rounded tool boxes, tabbed header, compact footer and a /usage dashboard.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -13,7 +13,7 @@
13
13
  import { readFile, writeFile } from "node:fs/promises";
14
14
  import { dirname, join } from "node:path";
15
15
  import { fileURLToPath } from "node:url";
16
- import { hexToOklch, oklchToRgb } from "../lib/color.ts";
16
+ import { parseOklch, oklchToRgb } from "../lib/color.ts";
17
17
  import { assert } from "../lib/check.ts";
18
18
 
19
19
  const HERE = dirname(fileURLToPath(import.meta.url));
@@ -44,13 +44,14 @@ const ACCENT_OFFSET = { heading: 0, link: 0, label: 60 };
44
44
  // --- theme parameters ------------------------------------------------------
45
45
 
46
46
  /**
47
- * `accent` seeds only the hue; `chroma` is the normalized accent chroma.
47
+ * `accent` is the seed color (OKLCH, hue + reference lightness/chroma) — a short
48
+ * vector, not a styling value; `chroma` is the normalized accent chroma.
48
49
  * `hues` overrides the derived hue per role (keyword/func/… /heading/link/label)
49
50
  * to keep the strong palettes recognisable.
50
51
  */
51
52
  const THEMES = {
52
53
  arnative: {
53
- accent: "#00d7ff",
54
+ accent: "oklch(81.1% 0.1455 217.709375)",
54
55
  chroma: 0.13,
55
56
  canvasL: 0.27,
56
57
  neutralHue: 232,
@@ -58,7 +59,7 @@ const THEMES = {
58
59
  hues: { heading: 75, label: 282 },
59
60
  },
60
61
  "arnative-sun": {
61
- accent: "#ffb454",
62
+ accent: "oklch(82.4% 0.1408 70.170730)",
62
63
  chroma: 0.14,
63
64
  canvasL: 0.27,
64
65
  neutralHue: 70,
@@ -66,7 +67,7 @@ const THEMES = {
66
67
  hues: { heading: 60, label: 330 },
67
68
  },
68
69
  "arnative-zinc": {
69
- accent: "#a1a1aa",
70
+ accent: "oklch(71.2% 0.0129 286.066504)",
70
71
  chroma: 0.02,
71
72
  canvasL: 0.26,
72
73
  neutralHue: 285,
@@ -74,7 +75,7 @@ const THEMES = {
74
75
  hues: { heading: 285, label: 285 },
75
76
  },
76
77
  "arnative-violet": {
77
- accent: "#a78bfa",
78
+ accent: "oklch(70.9% 0.1592 293.541199)",
78
79
  chroma: 0.14,
79
80
  canvasL: 0.27,
80
81
  neutralHue: 295,
@@ -82,7 +83,7 @@ const THEMES = {
82
83
  hues: { heading: 295, label: 330 },
83
84
  },
84
85
  "arnative-emerald": {
85
- accent: "#6ee7b7",
86
+ accent: "oklch(84.5% 0.1299 164.978166)",
86
87
  chroma: 0.12,
87
88
  canvasL: 0.27,
88
89
  neutralHue: 165,
@@ -90,7 +91,7 @@ const THEMES = {
90
91
  hues: { heading: 165, label: 200 },
91
92
  },
92
93
  "arnative-matrix": {
93
- accent: "#00ff41",
94
+ accent: "oklch(86.9% 0.2776 144.466116)",
94
95
  chroma: 0.22,
95
96
  canvasL: 0.15,
96
97
  neutralHue: 145,
@@ -101,7 +102,7 @@ const THEMES = {
101
102
  },
102
103
  },
103
104
  "arnative-cyberpunk": {
104
- accent: "#ff2e97",
105
+ accent: "oklch(66.7% 0.2492 356.749230)",
105
106
  chroma: 0.2,
106
107
  canvasL: 0.16,
107
108
  neutralHue: 275,
@@ -112,7 +113,7 @@ const THEMES = {
112
113
  },
113
114
  },
114
115
  "arnative-synthwave": {
115
- accent: "#ff7edb",
116
+ accent: "oklch(76.9% 0.1889 339.113944)",
116
117
  chroma: 0.17,
117
118
  canvasL: 0.22,
118
119
  neutralHue: 300,
@@ -123,7 +124,7 @@ const THEMES = {
123
124
  },
124
125
  },
125
126
  "arnative-gruvbox": {
126
- accent: "#fabd2f",
127
+ accent: "oklch(83.3% 0.1595 82.986608)",
127
128
  chroma: 0.14,
128
129
  canvasL: 0.24,
129
130
  neutralHue: 70,
@@ -134,7 +135,7 @@ const THEMES = {
134
135
  },
135
136
  },
136
137
  "arnative-nord": {
137
- accent: "#88c0d0",
138
+ accent: "oklch(77.5% 0.0622 217.469017)",
138
139
  chroma: 0.07,
139
140
  canvasL: 0.3,
140
141
  neutralHue: 240,
@@ -145,7 +146,7 @@ const THEMES = {
145
146
  },
146
147
  },
147
148
  "arnative-dracula": {
148
- accent: "#bd93f9",
149
+ accent: "oklch(74.2% 0.1485 301.883095)",
149
150
  chroma: 0.14,
150
151
  canvasL: 0.29,
151
152
  neutralHue: 285,
@@ -177,7 +178,9 @@ const ROLE_ORDER = [
177
178
  // --- derivation ------------------------------------------------------------
178
179
 
179
180
  function derive(spec) {
180
- const h = hexToOklch(spec.accent).H;
181
+ const seed = parseOklch(spec.accent);
182
+ assert(seed !== null, `theme accent must be an oklch color: ${spec.accent}`);
183
+ const h = seed.H;
181
184
  const c = spec.chroma;
182
185
  const nh = spec.neutralHue;
183
186
  const nc = spec.neutralChroma;
@@ -285,7 +288,9 @@ function derive(spec) {
285
288
  // Distinct vars must stay visually distinct (roles may alias on purpose).
286
289
  const seen = new Map();
287
290
  for (const [name, value] of Object.entries(vars)) {
288
- const { r, g, b } = oklchToRgb(...parseOklch(value));
291
+ const v = parseOklch(value);
292
+ assert(v !== null, `var ${name} is not an oklch color: ${value}`);
293
+ const { r, g, b } = oklchToRgb(v.L, v.C, v.H);
289
294
  const rgb = `${r},${g},${b}`;
290
295
  if (seen.has(rgb)) throw new Error(`vars ${seen.get(rgb)} and ${name} resolve to the same color`);
291
296
  seen.set(rgb, name);
@@ -294,11 +299,6 @@ function derive(spec) {
294
299
  return { vars, roles, exp };
295
300
  }
296
301
 
297
- function parseOklch(value) {
298
- const m = /\(([\d.]+)% ([\d.]+) ([\d.]+)\)/.exec(value);
299
- return [Number(m[1]) / 100, Number(m[2]), Number(m[3])];
300
- }
301
-
302
302
  // --- rendering -------------------------------------------------------------
303
303
 
304
304
  function render(name, { vars, roles, exp }) {