@bismawy/pi-arnative 0.4.0 → 0.5.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.
@@ -17,8 +17,9 @@ 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));
20
- const SCHEMA =
21
- "https://raw.githubusercontent.com/earendil-works/pi/main/packages/coding-agent/src/modes/interactive/theme/theme-schema.json";
20
+ // Pi 1.2.0 moved the published schema to pi.dev; older releases validated this
21
+ // reference only as a string, so the new URL is safe on both.
22
+ const SCHEMA = "https://pi.dev/schemas/theme.schema.json";
22
23
 
23
24
  // --- color helpers ---------------------------------------------------------
24
25
 
@@ -31,6 +32,23 @@ function oklch(L, C, H) {
31
32
  return `oklch(${+(L * 100).toFixed(1)}% ${round3(chroma)} ${Math.round(hue(H))})`;
32
33
  }
33
34
 
35
+ /**
36
+ * Soft tier of a color: clone at L 0.82 with a fifth of the GAMUT-CLAMPED chroma, hue
37
+ * kept. `lib/ansi.ts` softens the resolved token with this same recipe at runtime, so a
38
+ * `*Soft` var declared here and the extension's derived value agree. Grey-collapse is
39
+ * avoided because the hue is carried, not dropped.
40
+ *
41
+ * The chroma keeps five decimals (not the usual three): the soft tier multiplies it by
42
+ * 0.18, so a coarser round-trip shifts the last hex digit and the `declare == derive`
43
+ * guard in ui-render-tweaks.ts fails.
44
+ */
45
+ function soften(value) {
46
+ const o = parseOklch(value);
47
+ assert(o !== null, `cannot soften a non-oklch color: ${value}`);
48
+ const { chroma } = oklchToRgb(0.82, o.C * 0.18, o.H);
49
+ return `oklch(82% ${+chroma.toFixed(5)} ${Math.round(hue(o.H))})`;
50
+ }
51
+
34
52
  // --- shared ramps ----------------------------------------------------------
35
53
 
36
54
  const NEUTRAL = { fg: 0.88, fgMuted: 0.72, fgDim: 0.61, fgFaint: 0.49 };
@@ -38,16 +56,26 @@ const THINKING = { off: 0.4, minimal: 0.47, low: 0.55, medium: 0.63, high: 0.71,
38
56
  const SEMANTIC = { success: 145, error: 25, warning: 85 };
39
57
  const SYNTAX_OFFSET = { keyword: 0, func: 45, variable: -45, string: 120, number: 180, type: 90 };
40
58
 
41
- /** Default hue for a role used outside `syntax*` (heading/link/label). */
42
- const ACCENT_OFFSET = { heading: 0, link: 0, label: 60 };
59
+ /** Default hue for a role used outside `syntax*` (heading/link/label/code/bullet/border). */
60
+ const ACCENT_OFFSET = {
61
+ heading: 0,
62
+ link: 0,
63
+ label: 60,
64
+ code: 0,
65
+ bullet: 0,
66
+ border: 0,
67
+ selected: 0,
68
+ searchMatch: 0,
69
+ };
43
70
 
44
71
  // --- theme parameters ------------------------------------------------------
45
72
 
46
73
  /**
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.
49
- * `hues` overrides the derived hue per role (keyword/func/… /heading/link/label)
50
- * to keep the strong palettes recognisable.
74
+ * `accent` is the seed/focus color (OKLCH, hue + reference lightness/chroma);
75
+ * `primary` and `secondary` define the theme's core signature and dual-tone balance;
76
+ * `chroma` is the normalized accent chroma.
77
+ * `hues` overrides the derived hue per role (keyword/func/… /heading/link/label/code/…)
78
+ * to keep rich multi-colored palettes (cyberpunk, synthwave, dracula, etc.) authentic.
51
79
  */
52
80
  const THEMES = {
53
81
  arnative: {
@@ -56,6 +84,8 @@ const THEMES = {
56
84
  canvasL: 0.27,
57
85
  neutralHue: 232,
58
86
  neutralChroma: 0.012,
87
+ primary: "oklch(82% 0.14 218)",
88
+ secondary: "oklch(78% 0.11 282)",
59
89
  hues: { heading: 75, label: 282 },
60
90
  },
61
91
  "arnative-sun": {
@@ -64,6 +94,8 @@ const THEMES = {
64
94
  canvasL: 0.27,
65
95
  neutralHue: 70,
66
96
  neutralChroma: 0.012,
97
+ primary: "oklch(84% 0.15 70)",
98
+ secondary: "oklch(78% 0.12 35)",
67
99
  hues: { heading: 60, label: 330 },
68
100
  },
69
101
  "arnative-zinc": {
@@ -72,6 +104,8 @@ const THEMES = {
72
104
  canvasL: 0.26,
73
105
  neutralHue: 285,
74
106
  neutralChroma: 0.005,
107
+ primary: "oklch(86% 0.015 285)",
108
+ secondary: "oklch(72% 0.025 240)",
75
109
  hues: { heading: 285, label: 285 },
76
110
  },
77
111
  "arnative-violet": {
@@ -80,6 +114,8 @@ const THEMES = {
80
114
  canvasL: 0.27,
81
115
  neutralHue: 295,
82
116
  neutralChroma: 0.012,
117
+ primary: "oklch(82% 0.15 294)",
118
+ secondary: "oklch(78% 0.11 215)",
83
119
  hues: { heading: 295, label: 330 },
84
120
  },
85
121
  "arnative-emerald": {
@@ -88,80 +124,119 @@ const THEMES = {
88
124
  canvasL: 0.27,
89
125
  neutralHue: 165,
90
126
  neutralChroma: 0.012,
127
+ primary: "oklch(84% 0.13 165)",
128
+ secondary: "oklch(78% 0.10 195)",
91
129
  hues: { heading: 165, label: 200 },
92
130
  },
93
131
  "arnative-matrix": {
94
132
  accent: "oklch(86.9% 0.2776 144.466116)",
95
133
  chroma: 0.22,
96
- canvasL: 0.15,
134
+ canvasL: 0.24,
97
135
  neutralHue: 145,
98
136
  neutralChroma: 0.02,
137
+ primary: "oklch(88% 0.24 145)",
138
+ secondary: "oklch(76% 0.16 165)",
139
+ surfaceMsg: "oklch(24% 0.035 145)",
99
140
  hues: {
100
141
  heading: 145, link: 145, label: 145,
101
142
  keyword: 145, func: 145, variable: 145, string: 145, number: 145, type: 145,
102
143
  },
103
144
  },
104
145
  "arnative-cyberpunk": {
105
- accent: "oklch(66.7% 0.2492 356.749230)",
146
+ accent: "oklch(76% 0.22 355)",
106
147
  chroma: 0.2,
107
- canvasL: 0.16,
148
+ canvasL: 0.24,
108
149
  neutralHue: 275,
109
150
  neutralChroma: 0.02,
151
+ primary: "oklch(88% 0.18 95)",
152
+ secondary: "oklch(82% 0.16 200)",
153
+ surfaceMsg: "oklch(24% 0.035 200)",
154
+ borderAccent: "secondary",
155
+ headingColor: "primary",
156
+ codeColor: "secondary",
157
+ bulletColor: "primary",
158
+ keywordColor: "secondary",
110
159
  hues: {
111
- heading: 60, link: 190, label: 190,
112
- keyword: 190, func: 60, variable: 320, string: 320, number: 30, type: 150,
160
+ heading: 95, link: 200, label: 200, code: 200, bullet: 95, border: 200, selected: 200, searchMatch: 95,
161
+ keyword: 200, func: 95, variable: 355, string: 140, number: 45, type: 200,
113
162
  },
114
163
  },
115
164
  "arnative-synthwave": {
116
- accent: "oklch(76.9% 0.1889 339.113944)",
165
+ accent: "oklch(80% 0.18 50)",
117
166
  chroma: 0.17,
118
- canvasL: 0.22,
167
+ canvasL: 0.24,
119
168
  neutralHue: 300,
120
169
  neutralChroma: 0.015,
170
+ primary: "oklch(78% 0.19 339)",
171
+ secondary: "oklch(82% 0.16 185)",
172
+ surfaceMsg: "oklch(24% 0.03 300)",
173
+ borderAccent: "secondary",
174
+ codeColor: "secondary",
175
+ bulletColor: "primary",
121
176
  hues: {
122
- heading: 55, link: 180, label: 180,
123
- keyword: 55, func: 180, variable: 280, string: 30, number: 350, type: 320,
177
+ heading: 50, link: 185, label: 185, code: 185, bullet: 339, border: 185, selected: 185, searchMatch: 50,
178
+ keyword: 185, func: 50, variable: 339, string: 45, number: 339, type: 290,
124
179
  },
125
180
  },
126
181
  "arnative-gruvbox": {
127
- accent: "oklch(83.3% 0.1595 82.986608)",
182
+ accent: "oklch(76% 0.15 45)",
128
183
  chroma: 0.14,
129
184
  canvasL: 0.24,
130
185
  neutralHue: 70,
131
186
  neutralChroma: 0.015,
187
+ primary: "oklch(84% 0.14 83)",
188
+ secondary: "oklch(78% 0.10 175)",
189
+ borderAccent: "secondary",
190
+ headingColor: "primary",
191
+ codeColor: "secondary",
192
+ bulletColor: "accent",
132
193
  hues: {
133
- heading: 45, link: 200, label: 330,
134
- keyword: 25, func: 70, variable: 200, string: 70, number: 330, type: 45,
194
+ heading: 83, link: 175, label: 175, code: 175, bullet: 45, border: 83, selected: 83, searchMatch: 83,
195
+ keyword: 45, func: 83, variable: 175, string: 145, number: 45, type: 83,
135
196
  },
136
197
  },
137
198
  "arnative-nord": {
138
- accent: "oklch(77.5% 0.0622 217.469017)",
199
+ accent: "oklch(74% 0.09 310)",
139
200
  chroma: 0.07,
140
201
  canvasL: 0.3,
141
202
  neutralHue: 240,
142
203
  neutralChroma: 0.01,
204
+ primary: "oklch(78% 0.08 217)",
205
+ secondary: "oklch(82% 0.09 195)",
206
+ borderAccent: "secondary",
207
+ codeColor: "secondary",
208
+ bulletColor: "primary",
143
209
  hues: {
144
- heading: 40, link: 195, label: 310,
145
- keyword: 220, func: 195, variable: 220, string: 95, number: 310, type: 195,
210
+ heading: 75, link: 195, label: 195, code: 195, bullet: 217, border: 217, selected: 217, searchMatch: 75,
211
+ keyword: 217, func: 195, variable: 217, string: 145, number: 310, type: 195,
146
212
  },
147
213
  },
148
214
  "arnative-dracula": {
149
- accent: "oklch(74.2% 0.1485 301.883095)",
215
+ accent: "oklch(76% 0.18 330)",
150
216
  chroma: 0.14,
151
217
  canvasL: 0.29,
152
218
  neutralHue: 285,
153
219
  neutralChroma: 0.012,
220
+ primary: "oklch(78% 0.15 295)",
221
+ secondary: "oklch(82% 0.13 190)",
222
+ borderAccent: "secondary",
223
+ codeColor: "secondary",
224
+ bulletColor: "accent",
154
225
  hues: {
155
- heading: 60, link: 190, label: 330,
156
- keyword: 330, func: 135, variable: 190, string: 60, number: 265, type: 190,
226
+ heading: 85, link: 190, label: 190, code: 190, bullet: 330, border: 295, selected: 295, searchMatch: 85,
227
+ keyword: 330, func: 140, variable: 190, string: 85, number: 295, type: 190,
157
228
  },
158
229
  },
159
230
  };
160
231
 
161
232
  // --- role order (matches the pi theme schema) ------------------------------
162
-
233
+ //
234
+ // Only tokens pi's schema declares may appear as keys here: since 1.2.0 the
235
+ // `colors` object is validated with `additionalProperties: false`, and an extra
236
+ // key rejects the whole theme at startup. Arnative's own colors (`accentSoft`,
237
+ // `primary`, `secondary`, …) live in `vars` and are referenced from these roles.
163
238
  const ROLE_ORDER = [
164
- "accent", "accentSoft", "border", "borderAccent", "borderMuted", "success", "error", "warning",
239
+ "accent", "border", "borderAccent", "borderMuted", "success", "error", "warning",
165
240
  "muted", "dim", "text", "thinkingText",
166
241
  "selectedBg", "scrollbarTrack", "scrollbarThumb", "searchMatchBg", "searchMatchText",
167
242
  "userMessageBg", "userMessageText", "customMessageBg", "customMessageText", "customMessageLabel",
@@ -189,26 +264,42 @@ function derive(spec) {
189
264
  const roleHue = (role) => hue(H[role]);
190
265
  const sem = (role) => hue(SEMANTIC[role]);
191
266
 
267
+ const primarySeed = spec.primary ? parseOklch(spec.primary) : null;
268
+ const secondarySeed = spec.secondary ? parseOklch(spec.secondary) : null;
269
+ const primaryVal = primarySeed ? oklch(primarySeed.L, primarySeed.C, primarySeed.H) : oklch(0.82, c, h);
270
+ const secondaryVal = secondarySeed ? oklch(secondarySeed.L, secondarySeed.C, secondarySeed.H) : oklch(0.78, Math.min(c * 0.8, 0.12), roleHue("label"));
271
+
272
+ // Soft counterpart of the border-accent tier — the footer's label, clock and status
273
+ // colour. Softened from the SAME value the `borderAccent` token resolves to, because
274
+ // that token is what extensions read back at runtime (`softSecondaryOf` in
275
+ // lib/ansi.ts). One source keeps the declared value and the runtime one identical.
276
+ const accentBorderVal = oklch(0.82, c * 0.6, h);
277
+ const accentVal = oklch(0.78, c, h);
278
+ const borderAccentRole = spec.borderAccent ?? "accentBorder";
279
+ const borderAccentVal = { accentBorder: accentBorderVal, secondary: secondaryVal }[borderAccentRole] ?? borderAccentRole;
280
+
192
281
  const vars = {
193
282
  fg: oklch(NEUTRAL.fg, nc, nh),
194
283
  fgMuted: oklch(NEUTRAL.fgMuted, nc, nh),
195
284
  fgDim: oklch(NEUTRAL.fgDim, nc, nh),
196
285
  fgFaint: oklch(NEUTRAL.fgFaint, nc, nh),
197
- accent: oklch(0.78, c, h),
198
- accentSoft: oklch(0.82, c * 0.18, h),
199
- accentBorder: oklch(0.82, c * 0.6, h),
286
+ primary: primaryVal,
287
+ secondary: secondaryVal,
288
+ secondarySoft: soften(borderAccentVal),
289
+ accent: accentVal,
290
+ accentSoft: soften(accentVal),
291
+ accentBorder: accentBorderVal,
200
292
  success: oklch(0.78, Math.min(c, 0.15), sem("success")),
201
293
  error: oklch(0.76, Math.min(c, 0.15), sem("error")),
202
294
  warning: oklch(0.86, Math.min(c, 0.14), sem("warning")),
203
- surfaceMsg: oklch(base, nc * 0.5, nh),
295
+ surfaceMsg: spec.surfaceMsg ?? oklch(base, nc * 0.5, nh),
204
296
  };
205
297
 
206
- const accentLiteral = oklch(0.8, c * 0.7, h);
298
+ const accentLiteral = oklch(0.8, Math.min(c * 0.7, 0.14), h);
207
299
  const roles = {
208
300
  accent: "accent",
209
- accentSoft: "accentSoft",
210
- border: oklch(0.55, c * 0.35, h),
211
- borderAccent: "accentBorder",
301
+ border: oklch(0.55, Math.min(c * 0.35, 0.08), roleHue("border")),
302
+ borderAccent: spec.borderAccent ?? "accentBorder",
212
303
  borderMuted: "fgFaint",
213
304
  success: "success",
214
305
  error: "error",
@@ -218,45 +309,45 @@ function derive(spec) {
218
309
  text: "fg",
219
310
  thinkingText: "fgMuted",
220
311
 
221
- selectedBg: oklch(base + 0.07, c * 0.25, h),
312
+ selectedBg: oklch(base + 0.07, Math.min(c * 0.25, 0.06), roleHue("selected")),
222
313
  scrollbarTrack: "fgFaint",
223
314
  scrollbarThumb: "accentSoft",
224
- searchMatchBg: oklch(base + 0.14, c * 0.45, h),
315
+ searchMatchBg: oklch(base + 0.14, Math.min(c * 0.45, 0.10), roleHue("searchMatch")),
225
316
  searchMatchText: "fg",
226
317
  userMessageBg: "surfaceMsg",
227
318
  userMessageText: "fg",
228
- customMessageBg: oklch(base + 0.01, c * 0.4, roleHue("label")),
319
+ customMessageBg: oklch(base + 0.01, Math.min(c * 0.4, 0.05), roleHue("label")),
229
320
  customMessageText: "fg",
230
- customMessageLabel: oklch(0.78, c * 0.7, roleHue("label")),
321
+ customMessageLabel: spec.labelColor ?? oklch(0.78, Math.min(c * 0.7, 0.14), roleHue("label")),
231
322
 
232
- toolPendingBg: oklch(base - 0.02, c * 0.3, h),
233
- toolSuccessBg: oklch(base - 0.03, c * 0.4, sem("success")),
234
- toolErrorBg: oklch(base - 0.03, c * 0.4, sem("error")),
323
+ toolPendingBg: oklch(base - 0.02, Math.min(c * 0.3, 0.05), h),
324
+ toolSuccessBg: oklch(base - 0.03, Math.min(c * 0.4, 0.06), sem("success")),
325
+ toolErrorBg: oklch(base - 0.03, Math.min(c * 0.4, 0.06), sem("error")),
235
326
  toolTitle: "fg",
236
327
  toolOutput: "fgMuted",
237
328
 
238
- mdHeading: oklch(0.88, c * 0.6, roleHue("heading")),
239
- mdLink: oklch(0.8, c * 0.7, roleHue("link")),
329
+ mdHeading: spec.headingColor ?? oklch(0.88, Math.min(c * 0.6, 0.14), roleHue("heading")),
330
+ mdLink: spec.linkColor ?? oklch(0.8, Math.min(c * 0.7, 0.14), roleHue("link")),
240
331
  mdLinkUrl: "fgDim",
241
- mdCode: accentLiteral,
332
+ mdCode: spec.codeColor ?? accentLiteral,
242
333
  mdCodeBlock: "success",
243
334
  mdCodeBlockBorder: "fgMuted",
244
335
  mdQuote: "fgMuted",
245
336
  mdQuoteBorder: "fgMuted",
246
337
  mdHr: "fgFaint",
247
- mdListBullet: accentLiteral,
338
+ mdListBullet: spec.bulletColor ?? accentLiteral,
248
339
 
249
340
  toolDiffAdded: "success",
250
341
  toolDiffRemoved: "error",
251
342
  toolDiffContext: "fgMuted",
252
343
 
253
344
  syntaxComment: "fgDim",
254
- syntaxKeyword: accentLiteral,
255
- syntaxFunction: oklch(0.86, c * 0.8, roleHue("func")),
256
- syntaxVariable: oklch(0.84, c * 0.55, roleHue("variable")),
257
- syntaxString: oklch(0.82, c * 0.7, roleHue("string")),
258
- syntaxNumber: oklch(0.8, c * 0.7, roleHue("number")),
259
- syntaxType: oklch(0.8, c * 0.8, roleHue("type")),
345
+ syntaxKeyword: spec.keywordColor ?? accentLiteral,
346
+ syntaxFunction: oklch(0.86, Math.min(c * 0.8, 0.16), roleHue("func")),
347
+ syntaxVariable: oklch(0.84, Math.min(c * 0.55, 0.14), roleHue("variable")),
348
+ syntaxString: oklch(0.82, Math.min(c * 0.7, 0.16), roleHue("string")),
349
+ syntaxNumber: oklch(0.8, Math.min(c * 0.7, 0.15), roleHue("number")),
350
+ syntaxType: oklch(0.8, Math.min(c * 0.8, 0.16), roleHue("type")),
260
351
  syntaxOperator: "fg",
261
352
  syntaxPunctuation: "fgMuted",
262
353
 
@@ -274,7 +365,7 @@ function derive(spec) {
274
365
  const exp = {
275
366
  pageBg: oklch(base - 0.06, nc, nh),
276
367
  cardBg: oklch(base - 0.03, nc, nh),
277
- infoBg: oklch(base + 0.05, c * 0.25, h),
368
+ infoBg: oklch(base + 0.05, Math.min(c * 0.25, 0.06), h),
278
369
  };
279
370
 
280
371
  // Roles must match ROLE_ORDER exactly and each value must be a var name or a literal.