@bismawy/pi-arnative 0.3.0 → 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 +16 -0
- package/README.md +0 -2
- package/extensions/arnative.ts +13 -9
- package/extensions/footer.ts +14 -12
- package/extensions/section-headers.ts +13 -11
- package/extensions/ui-render-tweaks.ts +6 -2
- package/lib/color.ts +17 -9
- package/package.json +2 -2
- package/themes/gen-themes.mjs +20 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
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
|
+
|
|
10
|
+
## [0.3.1] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- `/arnative headers` and `/arnative footers` picker rows are reordered built-in first (`Pi (system)`, `Arnative (Full)`) and the parenthetical suffix now renders in the theme's `dim` token, so the preset name carries the row and the qualifier reads as a footnote (`extensions/arnative.ts`).
|
|
14
|
+
- Preset ids renamed: `Pi` → `Pi (system)`. `Arnative (Full)` is still the default and the header/footer slot logic is unchanged.
|
|
15
|
+
- Picker subtitles no longer repeat the preset names: "Selection applies live — custom header or built-in."
|
|
16
|
+
- README shows only `assets/pi-arnative.webp` as the hero image; the full-width `assets/banner.webp` stays reserved for the pi.dev package page via `pi.image`.
|
|
17
|
+
- `package.json` `description` now leads with the README tagline ("Refined aesthetics. Cohesive tools. Built for Pi.") followed by the capability summary, per the `/arnative-pi` manifest standard — pi.dev/packages renders this field verbatim as the package card description.
|
|
18
|
+
|
|
3
19
|
## [0.3.0] - 2026-10-03
|
|
4
20
|
|
|
5
21
|
### Fixed
|
package/README.md
CHANGED
|
@@ -6,8 +6,6 @@ Refined aesthetics. Cohesive tools. Built for Pi.
|
|
|
6
6
|
[](https://www.npmjs.com/package/@bismawy/pi-arnative)
|
|
7
7
|
[](https://github.com/bismawy/pi-arnative)
|
|
8
8
|
|
|
9
|
-
<img src="https://raw.githubusercontent.com/bismawy/pi-arnative/main/assets/banner.webp" alt="Arnative" width="100%">
|
|
10
|
-
|
|
11
9
|

|
|
12
10
|
|
|
13
11
|
## Overview
|
package/extensions/arnative.ts
CHANGED
|
@@ -112,9 +112,13 @@ export class ListPicker implements Component {
|
|
|
112
112
|
const name = this.names[i]!;
|
|
113
113
|
const isSelected = i === this.selected;
|
|
114
114
|
const mark = name === this.applied ? "● " : " ";
|
|
115
|
-
const
|
|
115
|
+
const plain = truncateToWidth(`${isSelected ? "▸" : " "} ${mark}${name}`, width - 4);
|
|
116
|
+
// Preset rows end in a parenthetical (`Pi (system)`, `Arnative (Full)`): the suffix reads softer than the name.
|
|
117
|
+
const suffix = /(\(.*\))$/.exec(plain)?.[1] ?? "";
|
|
118
|
+
const head = plain.slice(0, plain.length - suffix.length);
|
|
119
|
+
const tail = suffix ? th.fg("dim", suffix) : "";
|
|
116
120
|
// Same rule as the select dialogs: marker accent, name soft.
|
|
117
|
-
out.push(row(isSelected ? th.fg("accent",
|
|
121
|
+
out.push(row(isSelected ? th.fg("accent", head.slice(0, 2 + mark.length)) + soft(head.slice(2 + mark.length)) + tail : th.fg("text", head) + tail));
|
|
118
122
|
}
|
|
119
123
|
out.push(row(""));
|
|
120
124
|
out.push(row(th.fg("dim", this.text.footer)));
|
|
@@ -168,7 +172,7 @@ const openThemes = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
|
168
172
|
|
|
169
173
|
const FOOTERS_PICKER_TEXT: ListPickerText = {
|
|
170
174
|
title: "Arnative · Footers",
|
|
171
|
-
subtitle: "Selection applies live —
|
|
175
|
+
subtitle: "Selection applies live — custom footer or built-in.",
|
|
172
176
|
footer: "[↑↓] Preview live [Enter] Keep [Esc] Revert",
|
|
173
177
|
};
|
|
174
178
|
|
|
@@ -204,7 +208,7 @@ const openFooters = async (ctx: ExtensionCommandContext): Promise<void> => {
|
|
|
204
208
|
|
|
205
209
|
const HEADERS_PICKER_TEXT: ListPickerText = {
|
|
206
210
|
title: "Arnative · Headers",
|
|
207
|
-
subtitle: "Selection applies live —
|
|
211
|
+
subtitle: "Selection applies live — custom header or built-in.",
|
|
208
212
|
footer: "[↑↓] Preview live [Enter] Keep [Esc] Revert",
|
|
209
213
|
};
|
|
210
214
|
|
|
@@ -341,19 +345,19 @@ if (isMain(import.meta.url)) {
|
|
|
341
345
|
assert(reverted === 1 && done === 2, "Esc reverts after preview");
|
|
342
346
|
|
|
343
347
|
// Headers reuses the same picker with its own copy and presets.
|
|
344
|
-
assert(HEADER_PRESETS[0] === "
|
|
348
|
+
assert(HEADER_PRESETS[0] === "Pi (system)", "built-in header preset is listed first");
|
|
345
349
|
const headerPicker = new ListPicker(fakeTheme, [...HEADER_PRESETS], "Arnative (Full)", { preview: () => {}, revert: () => {}, done: () => {}, requestRender: () => {} }, HEADERS_PICKER_TEXT);
|
|
346
350
|
const headerLines = headerPicker.render(50);
|
|
347
351
|
assert(headerLines.some((l) => l.includes("Arnative · Headers")), "headers picker title");
|
|
348
352
|
assert(headerLines.some((l) => l.includes("● Arnative (Full)")), "headers picker marks the active preset");
|
|
349
|
-
assert(headerLines.some((l) => l.includes(" Pi")), "headers picker lists
|
|
353
|
+
assert(headerLines.some((l) => l.includes(" Pi (system)")), "headers picker lists the built-in preset");
|
|
350
354
|
|
|
351
355
|
// Footers reuses the same picker too.
|
|
352
|
-
assert(FOOTER_PRESETS[0] === "
|
|
353
|
-
const footerPicker = new ListPicker(fakeTheme, [...FOOTER_PRESETS], "Pi", { preview: () => {}, revert: () => {}, done: () => {}, requestRender: () => {} }, FOOTERS_PICKER_TEXT);
|
|
356
|
+
assert(FOOTER_PRESETS[0] === "Pi (system)", "built-in footer preset is listed first");
|
|
357
|
+
const footerPicker = new ListPicker(fakeTheme, [...FOOTER_PRESETS], "Pi (system)", { preview: () => {}, revert: () => {}, done: () => {}, requestRender: () => {} }, FOOTERS_PICKER_TEXT);
|
|
354
358
|
const footerLines = footerPicker.render(50);
|
|
355
359
|
assert(footerLines.some((l) => l.includes("Arnative · Footers")), "footers picker title");
|
|
356
|
-
assert(footerLines.some((l) => l.includes("● Pi")), "footers picker marks the active preset");
|
|
360
|
+
assert(footerLines.some((l) => l.includes("● Pi (system)")), "footers picker marks the active preset");
|
|
357
361
|
|
|
358
362
|
console.log("arnative.ts self-check OK");
|
|
359
363
|
}
|
package/extensions/footer.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Rows 2-3: extension status (mcp first) ......... tok/s · cache · token,
|
|
5
5
|
* each side wrapped inside its half of the width and paired row by row.
|
|
6
6
|
*
|
|
7
|
-
* Preset (`/arnative footers`): "Arnative (Full)" (default) or "Pi" (built-in footer), live.
|
|
7
|
+
* Preset (`/arnative footers`): "Arnative (Full)" (default) or "Pi (system)" (built-in footer), live.
|
|
8
8
|
* Transcript clock & bubble bg: extensions/timestamps.ts
|
|
9
9
|
* /new header: extensions/section-headers.ts
|
|
10
10
|
* Selection + box reload: extensions/ui-render-tweaks.ts
|
|
@@ -27,10 +27,11 @@ import { formatTokens } from "../lib/usage-store.ts";
|
|
|
27
27
|
// fire this twice even when the terminal reports event types (Kitty flag 2).
|
|
28
28
|
|
|
29
29
|
// Footer preset chosen through `/arnative footers`. "Arnative (Full)" = this extension's
|
|
30
|
-
// 3-row grid footer; "Pi" = pi's built-in footer (the boxed editor stays arnative
|
|
31
|
-
// pi's `setEditorComponent()` would close the picker mid-preview). The picker
|
|
32
|
-
// arnative.ts, so the choice is shared through globalThis like the header preset.
|
|
33
|
-
|
|
30
|
+
// 3-row grid footer; "Pi (system)" = pi's built-in footer (the boxed editor stays arnative
|
|
31
|
+
// in both — pi's `setEditorComponent()` would close the picker mid-preview). The picker
|
|
32
|
+
// lives in arnative.ts, so the choice is shared through globalThis like the header preset.
|
|
33
|
+
// Listed built-in-first: the `(Full)` suffix is rendered dim, so the plain row reads cleaner on top.
|
|
34
|
+
export const FOOTER_PRESETS = ["Pi (system)", "Arnative (Full)"] as const;
|
|
34
35
|
export type FooterPreset = (typeof FOOTER_PRESETS)[number];
|
|
35
36
|
export const DEFAULT_FOOTER_PRESET: FooterPreset = "Arnative (Full)";
|
|
36
37
|
const FOOTER_PRESET_KEY = Symbol.for("pi-arnative.footerPreset");
|
|
@@ -47,7 +48,7 @@ export function activeFooterPreset(): FooterPreset {
|
|
|
47
48
|
* `disposeActiveSelector()`, which would close the `/arnative` picker mid-preview.
|
|
48
49
|
*/
|
|
49
50
|
export function applyFooterPreset(ctx: ExtensionContext): void {
|
|
50
|
-
if (activeFooterPreset() === "Pi") {
|
|
51
|
+
if (activeFooterPreset() === "Pi (system)") {
|
|
51
52
|
ctx.ui?.setFooter?.(undefined);
|
|
52
53
|
return;
|
|
53
54
|
}
|
|
@@ -67,8 +68,8 @@ if (FooterComponent?.prototype?.render && !(globalThis as Record<symbol, boolean
|
|
|
67
68
|
(globalThis as Record<symbol, boolean>)[PATCHED_KEY] = true;
|
|
68
69
|
const origRender = FooterComponent.prototype.render;
|
|
69
70
|
FooterComponent.prototype.render = function (width: number): string[] {
|
|
70
|
-
// "Pi" preset = pi's own footer, so keep its stock line count.
|
|
71
|
-
if (activeFooterPreset() === "Pi") return origRender.call(this, width);
|
|
71
|
+
// "Pi (system)" preset = pi's own footer, so keep its stock line count.
|
|
72
|
+
if (activeFooterPreset() === "Pi (system)") return origRender.call(this, width);
|
|
72
73
|
const lines = origRender.call(this, width);
|
|
73
74
|
return lines.length > 2 ? lines.slice(0, 2) : lines;
|
|
74
75
|
};
|
|
@@ -893,9 +894,10 @@ if (isMain(import.meta.url)) {
|
|
|
893
894
|
assert(git !== null && git.branch.length > 0, "queued git refresh re-ran on the newest cwd (not dropped)");
|
|
894
895
|
rmSync(noGit, { recursive: true, force: true });
|
|
895
896
|
|
|
896
|
-
// Footer presets: Arnative (Full) is the default; Pi hands the footer slot back to pi (the editor stays arnative).
|
|
897
|
+
// Footer presets: Arnative (Full) is the default; Pi (system) hands the footer slot back to pi (the editor stays arnative).
|
|
897
898
|
assert(DEFAULT_FOOTER_PRESET === "Arnative (Full)" && FOOTER_PRESETS.length === 2, "Arnative (Full) is the default footer preset");
|
|
898
|
-
assert(
|
|
899
|
+
assert(FOOTER_PRESETS.join(" | ") === "Pi (system) | Arnative (Full)", "built-in preset is listed first");
|
|
900
|
+
assert(markedLabels(FOOTER_PRESETS, "Pi (system)").join(" | ") === "● Pi (system) | Arnative (Full)", "footer preset labels mark the active one");
|
|
899
901
|
let footerCleared = 0;
|
|
900
902
|
let editorTouched = 0;
|
|
901
903
|
const presetCtx: any = {
|
|
@@ -904,8 +906,8 @@ if (isMain(import.meta.url)) {
|
|
|
904
906
|
setEditorComponent: () => { editorTouched++; },
|
|
905
907
|
},
|
|
906
908
|
};
|
|
907
|
-
setFooterPreset("Pi", presetCtx);
|
|
908
|
-
assert(footerCleared === 1 && editorTouched === 0 && activeFooterPreset() === "Pi", "Pi clears the footer only (an editor swap would close the picker)");
|
|
909
|
+
setFooterPreset("Pi (system)", presetCtx);
|
|
910
|
+
assert(footerCleared === 1 && editorTouched === 0 && activeFooterPreset() === "Pi (system)", "Pi (system) clears the footer only (an editor swap would close the picker)");
|
|
909
911
|
let registered = 0;
|
|
910
912
|
(globalThis as Record<symbol, (ctx: ExtensionContext) => void>)[FOOTER_REGISTRAR_KEY] = () => { registered++; };
|
|
911
913
|
setFooterPreset("Arnative (Full)", presetCtx);
|
|
@@ -298,9 +298,10 @@ export const tabStore: Map<TabKey, string[]> =
|
|
|
298
298
|
(globalThis as Record<symbol, Map<TabKey, string[]>>)[STORE_KEY] = tabStore;
|
|
299
299
|
|
|
300
300
|
// Header preset chosen through `/arnative headers`. "Arnative (Full)" = this extension's
|
|
301
|
-
// header; "Pi" = pi's built-in header. The picker lives in arnative.ts, so the
|
|
302
|
-
// shared through globalThis like the other cross-module state.
|
|
303
|
-
|
|
301
|
+
// header; "Pi (system)" = pi's built-in header. The picker lives in arnative.ts, so the
|
|
302
|
+
// choice is shared through globalThis like the other cross-module state.
|
|
303
|
+
// Listed built-in-first: the `(Full)` suffix is rendered dim, so the plain row reads cleaner on top.
|
|
304
|
+
export const HEADER_PRESETS = ["Pi (system)", "Arnative (Full)"] as const;
|
|
304
305
|
export type HeaderPreset = (typeof HEADER_PRESETS)[number];
|
|
305
306
|
export const DEFAULT_HEADER_PRESET: HeaderPreset = "Arnative (Full)";
|
|
306
307
|
const HEADER_PRESET_KEY = Symbol.for("pi-arnative.headerPreset");
|
|
@@ -310,9 +311,9 @@ export function activeHeaderPreset(): HeaderPreset {
|
|
|
310
311
|
return HEADER_PRESETS.includes(stored as HeaderPreset) ? (stored as HeaderPreset) : DEFAULT_HEADER_PRESET;
|
|
311
312
|
}
|
|
312
313
|
|
|
313
|
-
/** Hand the header slot to the active preset: arnative's box, or pi's built-in ("Pi"). */
|
|
314
|
+
/** Hand the header slot to the active preset: arnative's box, or pi's built-in ("Pi (system)"). */
|
|
314
315
|
export function applyHeaderPreset(ctx: ExtensionContext): void {
|
|
315
|
-
if (activeHeaderPreset() === "Pi") {
|
|
316
|
+
if (activeHeaderPreset() === "Pi (system)") {
|
|
316
317
|
ctx.ui?.setHeader?.(undefined);
|
|
317
318
|
} else {
|
|
318
319
|
ctx.ui?.setHeader?.((tui, theme) => new ArnativeHeader(tui, theme, ctx));
|
|
@@ -642,8 +643,8 @@ if (UserMessageComponent?.prototype && !(globalThis as Record<symbol, boolean>)[
|
|
|
642
643
|
if (typeof containerProto?.addChild === "function") {
|
|
643
644
|
const origAddChild = containerProto.addChild;
|
|
644
645
|
containerProto.addChild = function (child: any): unknown {
|
|
645
|
-
// "Pi" preset = pi's own header, so leave pi's startup list untouched.
|
|
646
|
-
if (activeHeaderPreset() === "Pi") return origAddChild.call(this, child);
|
|
646
|
+
// "Pi (system)" preset = pi's own header, so leave pi's startup list untouched.
|
|
647
|
+
if (activeHeaderPreset() === "Pi (system)") return origAddChild.call(this, child);
|
|
647
648
|
try {
|
|
648
649
|
const body = sectionBodyOf(child);
|
|
649
650
|
const sectionName = body === null ? null : sectionNameOf(body);
|
|
@@ -730,13 +731,14 @@ if (isMain(import.meta.url)) {
|
|
|
730
731
|
assert(sectionNameOf("\x1b[33m[Skills]\x1b[39m\n a, b") === "Skills", "header [Skills] detected");
|
|
731
732
|
assert(sectionNameOf("pi v0.87.1") === null, "non-section text is not detected");
|
|
732
733
|
|
|
733
|
-
// Header presets: Arnative (Full) is the default; Pi hands the header slot back to pi.
|
|
734
|
+
// Header presets: Arnative (Full) is the default; Pi (system) hands the header slot back to pi.
|
|
734
735
|
assert(DEFAULT_HEADER_PRESET === "Arnative (Full)" && HEADER_PRESETS.length === 2, "Arnative (Full) is the default header preset");
|
|
735
|
-
assert(
|
|
736
|
+
assert(HEADER_PRESETS.join(" | ") === "Pi (system) | Arnative (Full)", "built-in preset is listed first");
|
|
737
|
+
assert(markedLabels(HEADER_PRESETS, "Arnative (Full)").join(" | ") === " Pi (system) | ● Arnative (Full)", "preset labels mark the active one");
|
|
736
738
|
let installed: unknown = "untouched";
|
|
737
739
|
const presetCtx: any = { ui: { setHeader: (factory: unknown) => { installed = factory; } } };
|
|
738
|
-
setHeaderPreset("Pi", presetCtx);
|
|
739
|
-
assert(installed === undefined && activeHeaderPreset() === "Pi", "Pi clears the custom header");
|
|
740
|
+
setHeaderPreset("Pi (system)", presetCtx);
|
|
741
|
+
assert(installed === undefined && activeHeaderPreset() === "Pi (system)", "Pi (system) clears the custom header");
|
|
740
742
|
setHeaderPreset("Arnative (Full)", presetCtx);
|
|
741
743
|
assert(typeof installed === "function" && activeHeaderPreset() === "Arnative (Full)", "Arnative (Full) installs the header factory");
|
|
742
744
|
|
|
@@ -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
|
-
|
|
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,
|
|
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(
|
|
108
|
-
assert(toHex(
|
|
109
|
-
assert(toHex("oklch(100% 0 0)") ===
|
|
110
|
-
assert(oklchToHex(1, 0, 0) ===
|
|
111
|
-
assert(oklchToHex(0, 0, 0) ===
|
|
112
|
-
assert(contrast(
|
|
113
|
-
assert(Math.abs(contrast(
|
|
114
|
-
const back = hexToOklch(
|
|
115
|
-
assert(Math.abs(back.H - 218) < 6, `hexToOklch hue of
|
|
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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bismawy/pi-arnative",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|
|
7
7
|
"pi-theme",
|
package/themes/gen-themes.mjs
CHANGED
|
@@ -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 {
|
|
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`
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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
|
|
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
|
|
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 }) {
|