@bismawy/pi-arnative 0.3.10 → 0.5.1

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
 
@@ -38,16 +39,26 @@ const THINKING = { off: 0.4, minimal: 0.47, low: 0.55, medium: 0.63, high: 0.71,
38
39
  const SEMANTIC = { success: 145, error: 25, warning: 85 };
39
40
  const SYNTAX_OFFSET = { keyword: 0, func: 45, variable: -45, string: 120, number: 180, type: 90 };
40
41
 
41
- /** Default hue for a role used outside `syntax*` (heading/link/label). */
42
- const ACCENT_OFFSET = { heading: 0, link: 0, label: 60 };
42
+ /** Default hue for a role used outside `syntax*` (heading/link/label/code/bullet/border). */
43
+ const ACCENT_OFFSET = {
44
+ heading: 0,
45
+ link: 0,
46
+ label: 60,
47
+ code: 0,
48
+ bullet: 0,
49
+ border: 0,
50
+ selected: 0,
51
+ searchMatch: 0,
52
+ };
43
53
 
44
54
  // --- theme parameters ------------------------------------------------------
45
55
 
46
56
  /**
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.
57
+ * `accent` is the seed/focus color (OKLCH, hue + reference lightness/chroma);
58
+ * `primary` and `secondary` define the theme's core signature and dual-tone balance;
59
+ * `chroma` is the normalized accent chroma.
60
+ * `hues` overrides the derived hue per role (keyword/func/… /heading/link/label/code/…)
61
+ * to keep rich multi-colored palettes (cyberpunk, synthwave, dracula, etc.) authentic.
51
62
  */
52
63
  const THEMES = {
53
64
  arnative: {
@@ -56,6 +67,8 @@ const THEMES = {
56
67
  canvasL: 0.27,
57
68
  neutralHue: 232,
58
69
  neutralChroma: 0.012,
70
+ primary: "oklch(82% 0.14 218)",
71
+ secondary: "oklch(78% 0.11 282)",
59
72
  hues: { heading: 75, label: 282 },
60
73
  },
61
74
  "arnative-sun": {
@@ -64,6 +77,8 @@ const THEMES = {
64
77
  canvasL: 0.27,
65
78
  neutralHue: 70,
66
79
  neutralChroma: 0.012,
80
+ primary: "oklch(84% 0.15 70)",
81
+ secondary: "oklch(78% 0.12 35)",
67
82
  hues: { heading: 60, label: 330 },
68
83
  },
69
84
  "arnative-zinc": {
@@ -72,6 +87,8 @@ const THEMES = {
72
87
  canvasL: 0.26,
73
88
  neutralHue: 285,
74
89
  neutralChroma: 0.005,
90
+ primary: "oklch(86% 0.015 285)",
91
+ secondary: "oklch(72% 0.025 240)",
75
92
  hues: { heading: 285, label: 285 },
76
93
  },
77
94
  "arnative-violet": {
@@ -80,6 +97,8 @@ const THEMES = {
80
97
  canvasL: 0.27,
81
98
  neutralHue: 295,
82
99
  neutralChroma: 0.012,
100
+ primary: "oklch(82% 0.15 294)",
101
+ secondary: "oklch(78% 0.11 215)",
83
102
  hues: { heading: 295, label: 330 },
84
103
  },
85
104
  "arnative-emerald": {
@@ -88,80 +107,119 @@ const THEMES = {
88
107
  canvasL: 0.27,
89
108
  neutralHue: 165,
90
109
  neutralChroma: 0.012,
110
+ primary: "oklch(84% 0.13 165)",
111
+ secondary: "oklch(78% 0.10 195)",
91
112
  hues: { heading: 165, label: 200 },
92
113
  },
93
114
  "arnative-matrix": {
94
115
  accent: "oklch(86.9% 0.2776 144.466116)",
95
116
  chroma: 0.22,
96
- canvasL: 0.15,
117
+ canvasL: 0.24,
97
118
  neutralHue: 145,
98
119
  neutralChroma: 0.02,
120
+ primary: "oklch(88% 0.24 145)",
121
+ secondary: "oklch(76% 0.16 165)",
122
+ surfaceMsg: "oklch(24% 0.035 145)",
99
123
  hues: {
100
124
  heading: 145, link: 145, label: 145,
101
125
  keyword: 145, func: 145, variable: 145, string: 145, number: 145, type: 145,
102
126
  },
103
127
  },
104
128
  "arnative-cyberpunk": {
105
- accent: "oklch(66.7% 0.2492 356.749230)",
129
+ accent: "oklch(76% 0.22 355)",
106
130
  chroma: 0.2,
107
- canvasL: 0.16,
131
+ canvasL: 0.24,
108
132
  neutralHue: 275,
109
133
  neutralChroma: 0.02,
134
+ primary: "oklch(88% 0.18 95)",
135
+ secondary: "oklch(82% 0.16 200)",
136
+ surfaceMsg: "oklch(24% 0.035 200)",
137
+ borderAccent: "secondary",
138
+ headingColor: "primary",
139
+ codeColor: "secondary",
140
+ bulletColor: "primary",
141
+ keywordColor: "secondary",
110
142
  hues: {
111
- heading: 60, link: 190, label: 190,
112
- keyword: 190, func: 60, variable: 320, string: 320, number: 30, type: 150,
143
+ heading: 95, link: 200, label: 200, code: 200, bullet: 95, border: 200, selected: 200, searchMatch: 95,
144
+ keyword: 200, func: 95, variable: 355, string: 140, number: 45, type: 200,
113
145
  },
114
146
  },
115
147
  "arnative-synthwave": {
116
- accent: "oklch(76.9% 0.1889 339.113944)",
148
+ accent: "oklch(80% 0.18 50)",
117
149
  chroma: 0.17,
118
- canvasL: 0.22,
150
+ canvasL: 0.24,
119
151
  neutralHue: 300,
120
152
  neutralChroma: 0.015,
153
+ primary: "oklch(78% 0.19 339)",
154
+ secondary: "oklch(82% 0.16 185)",
155
+ surfaceMsg: "oklch(24% 0.03 300)",
156
+ borderAccent: "secondary",
157
+ codeColor: "secondary",
158
+ bulletColor: "primary",
121
159
  hues: {
122
- heading: 55, link: 180, label: 180,
123
- keyword: 55, func: 180, variable: 280, string: 30, number: 350, type: 320,
160
+ heading: 50, link: 185, label: 185, code: 185, bullet: 339, border: 185, selected: 185, searchMatch: 50,
161
+ keyword: 185, func: 50, variable: 339, string: 45, number: 339, type: 290,
124
162
  },
125
163
  },
126
164
  "arnative-gruvbox": {
127
- accent: "oklch(83.3% 0.1595 82.986608)",
165
+ accent: "oklch(76% 0.15 45)",
128
166
  chroma: 0.14,
129
167
  canvasL: 0.24,
130
168
  neutralHue: 70,
131
169
  neutralChroma: 0.015,
170
+ primary: "oklch(84% 0.14 83)",
171
+ secondary: "oklch(78% 0.10 175)",
172
+ borderAccent: "secondary",
173
+ headingColor: "primary",
174
+ codeColor: "secondary",
175
+ bulletColor: "accent",
132
176
  hues: {
133
- heading: 45, link: 200, label: 330,
134
- keyword: 25, func: 70, variable: 200, string: 70, number: 330, type: 45,
177
+ heading: 83, link: 175, label: 175, code: 175, bullet: 45, border: 83, selected: 83, searchMatch: 83,
178
+ keyword: 45, func: 83, variable: 175, string: 145, number: 45, type: 83,
135
179
  },
136
180
  },
137
181
  "arnative-nord": {
138
- accent: "oklch(77.5% 0.0622 217.469017)",
182
+ accent: "oklch(74% 0.09 310)",
139
183
  chroma: 0.07,
140
184
  canvasL: 0.3,
141
185
  neutralHue: 240,
142
186
  neutralChroma: 0.01,
187
+ primary: "oklch(78% 0.08 217)",
188
+ secondary: "oklch(82% 0.09 195)",
189
+ borderAccent: "secondary",
190
+ codeColor: "secondary",
191
+ bulletColor: "primary",
143
192
  hues: {
144
- heading: 40, link: 195, label: 310,
145
- keyword: 220, func: 195, variable: 220, string: 95, number: 310, type: 195,
193
+ heading: 75, link: 195, label: 195, code: 195, bullet: 217, border: 217, selected: 217, searchMatch: 75,
194
+ keyword: 217, func: 195, variable: 217, string: 145, number: 310, type: 195,
146
195
  },
147
196
  },
148
197
  "arnative-dracula": {
149
- accent: "oklch(74.2% 0.1485 301.883095)",
198
+ accent: "oklch(76% 0.18 330)",
150
199
  chroma: 0.14,
151
200
  canvasL: 0.29,
152
201
  neutralHue: 285,
153
202
  neutralChroma: 0.012,
203
+ primary: "oklch(78% 0.15 295)",
204
+ secondary: "oklch(82% 0.13 190)",
205
+ borderAccent: "secondary",
206
+ codeColor: "secondary",
207
+ bulletColor: "accent",
154
208
  hues: {
155
- heading: 60, link: 190, label: 330,
156
- keyword: 330, func: 135, variable: 190, string: 60, number: 265, type: 190,
209
+ heading: 85, link: 190, label: 190, code: 190, bullet: 330, border: 295, selected: 295, searchMatch: 85,
210
+ keyword: 330, func: 140, variable: 190, string: 85, number: 295, type: 190,
157
211
  },
158
212
  },
159
213
  };
160
214
 
161
215
  // --- role order (matches the pi theme schema) ------------------------------
162
-
216
+ //
217
+ // Only tokens pi's schema declares may appear as keys here: since 1.2.0 the
218
+ // `colors` object is validated with `additionalProperties: false`, and an extra
219
+ // key rejects the whole theme at startup. Arnative's own colors (`accentSoft`,
220
+ // `primary`, `secondary`, …) live in `vars` and are referenced from these roles.
163
221
  const ROLE_ORDER = [
164
- "accent", "accentSoft", "border", "borderAccent", "borderMuted", "success", "error", "warning",
222
+ "accent", "border", "borderAccent", "borderMuted", "success", "error", "warning",
165
223
  "muted", "dim", "text", "thinkingText",
166
224
  "selectedBg", "scrollbarTrack", "scrollbarThumb", "searchMatchBg", "searchMatchText",
167
225
  "userMessageBg", "userMessageText", "customMessageBg", "customMessageText", "customMessageLabel",
@@ -189,26 +247,37 @@ function derive(spec) {
189
247
  const roleHue = (role) => hue(H[role]);
190
248
  const sem = (role) => hue(SEMANTIC[role]);
191
249
 
250
+ const primarySeed = spec.primary ? parseOklch(spec.primary) : null;
251
+ const secondarySeed = spec.secondary ? parseOklch(spec.secondary) : null;
252
+ const primaryVal = primarySeed ? oklch(primarySeed.L, primarySeed.C, primarySeed.H) : oklch(0.82, c, h);
253
+ const secondaryVal = secondarySeed ? oklch(secondarySeed.L, secondarySeed.C, secondarySeed.H) : oklch(0.78, Math.min(c * 0.8, 0.12), roleHue("label"));
254
+
192
255
  const vars = {
193
256
  fg: oklch(NEUTRAL.fg, nc, nh),
194
257
  fgMuted: oklch(NEUTRAL.fgMuted, nc, nh),
195
258
  fgDim: oklch(NEUTRAL.fgDim, nc, nh),
196
259
  fgFaint: oklch(NEUTRAL.fgFaint, nc, nh),
260
+ primary: primaryVal,
261
+ secondary: secondaryVal,
197
262
  accent: oklch(0.78, c, h),
198
- accentSoft: oklch(0.82, c * 0.18, h),
263
+ // Derived from the accent's GAMUT-CLAMPED chroma, not the raw spec chroma. Pi's schema
264
+ // forbids custom keys in `colors`, so extensions resolve the soft tier at runtime by
265
+ // softening the resolved `accent` (see `softOf` in lib/ansi.ts). Building it from the
266
+ // same clamped chroma makes the declared value and the runtime one agree; using `c`
267
+ // drifts wherever the raw chroma is out of gamut at the accent's lightness.
268
+ accentSoft: oklch(0.82, oklchToRgb(0.78, c, h).chroma * 0.18, h),
199
269
  accentBorder: oklch(0.82, c * 0.6, h),
200
270
  success: oklch(0.78, Math.min(c, 0.15), sem("success")),
201
271
  error: oklch(0.76, Math.min(c, 0.15), sem("error")),
202
272
  warning: oklch(0.86, Math.min(c, 0.14), sem("warning")),
203
- surfaceMsg: oklch(base, nc * 0.5, nh),
273
+ surfaceMsg: spec.surfaceMsg ?? oklch(base, nc * 0.5, nh),
204
274
  };
205
275
 
206
- const accentLiteral = oklch(0.8, c * 0.7, h);
276
+ const accentLiteral = oklch(0.8, Math.min(c * 0.7, 0.14), h);
207
277
  const roles = {
208
278
  accent: "accent",
209
- accentSoft: "accentSoft",
210
- border: oklch(0.55, c * 0.35, h),
211
- borderAccent: "accentBorder",
279
+ border: oklch(0.55, Math.min(c * 0.35, 0.08), roleHue("border")),
280
+ borderAccent: spec.borderAccent ?? "accentBorder",
212
281
  borderMuted: "fgFaint",
213
282
  success: "success",
214
283
  error: "error",
@@ -218,45 +287,45 @@ function derive(spec) {
218
287
  text: "fg",
219
288
  thinkingText: "fgMuted",
220
289
 
221
- selectedBg: oklch(base + 0.07, c * 0.25, h),
290
+ selectedBg: oklch(base + 0.07, Math.min(c * 0.25, 0.06), roleHue("selected")),
222
291
  scrollbarTrack: "fgFaint",
223
292
  scrollbarThumb: "accentSoft",
224
- searchMatchBg: oklch(base + 0.14, c * 0.45, h),
293
+ searchMatchBg: oklch(base + 0.14, Math.min(c * 0.45, 0.10), roleHue("searchMatch")),
225
294
  searchMatchText: "fg",
226
295
  userMessageBg: "surfaceMsg",
227
296
  userMessageText: "fg",
228
- customMessageBg: oklch(base + 0.01, c * 0.4, roleHue("label")),
297
+ customMessageBg: oklch(base + 0.01, Math.min(c * 0.4, 0.05), roleHue("label")),
229
298
  customMessageText: "fg",
230
- customMessageLabel: oklch(0.78, c * 0.7, roleHue("label")),
299
+ customMessageLabel: spec.labelColor ?? oklch(0.78, Math.min(c * 0.7, 0.14), roleHue("label")),
231
300
 
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")),
301
+ toolPendingBg: oklch(base - 0.02, Math.min(c * 0.3, 0.05), h),
302
+ toolSuccessBg: oklch(base - 0.03, Math.min(c * 0.4, 0.06), sem("success")),
303
+ toolErrorBg: oklch(base - 0.03, Math.min(c * 0.4, 0.06), sem("error")),
235
304
  toolTitle: "fg",
236
305
  toolOutput: "fgMuted",
237
306
 
238
- mdHeading: oklch(0.88, c * 0.6, roleHue("heading")),
239
- mdLink: oklch(0.8, c * 0.7, roleHue("link")),
307
+ mdHeading: spec.headingColor ?? oklch(0.88, Math.min(c * 0.6, 0.14), roleHue("heading")),
308
+ mdLink: spec.linkColor ?? oklch(0.8, Math.min(c * 0.7, 0.14), roleHue("link")),
240
309
  mdLinkUrl: "fgDim",
241
- mdCode: accentLiteral,
310
+ mdCode: spec.codeColor ?? accentLiteral,
242
311
  mdCodeBlock: "success",
243
312
  mdCodeBlockBorder: "fgMuted",
244
313
  mdQuote: "fgMuted",
245
314
  mdQuoteBorder: "fgMuted",
246
315
  mdHr: "fgFaint",
247
- mdListBullet: accentLiteral,
316
+ mdListBullet: spec.bulletColor ?? accentLiteral,
248
317
 
249
318
  toolDiffAdded: "success",
250
319
  toolDiffRemoved: "error",
251
320
  toolDiffContext: "fgMuted",
252
321
 
253
322
  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")),
323
+ syntaxKeyword: spec.keywordColor ?? accentLiteral,
324
+ syntaxFunction: oklch(0.86, Math.min(c * 0.8, 0.16), roleHue("func")),
325
+ syntaxVariable: oklch(0.84, Math.min(c * 0.55, 0.14), roleHue("variable")),
326
+ syntaxString: oklch(0.82, Math.min(c * 0.7, 0.16), roleHue("string")),
327
+ syntaxNumber: oklch(0.8, Math.min(c * 0.7, 0.15), roleHue("number")),
328
+ syntaxType: oklch(0.8, Math.min(c * 0.8, 0.16), roleHue("type")),
260
329
  syntaxOperator: "fg",
261
330
  syntaxPunctuation: "fgMuted",
262
331
 
@@ -274,7 +343,7 @@ function derive(spec) {
274
343
  const exp = {
275
344
  pageBg: oklch(base - 0.06, nc, nh),
276
345
  cardBg: oklch(base - 0.03, nc, nh),
277
- infoBg: oklch(base + 0.05, c * 0.25, h),
346
+ infoBg: oklch(base + 0.05, Math.min(c * 0.25, 0.06), h),
278
347
  };
279
348
 
280
349
  // Roles must match ROLE_ORDER exactly and each value must be a var name or a literal.