@intentius/behold 0.7.0 → 0.9.0
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/AGENTS.md +94 -2
- package/README.md +133 -2
- package/demos.json +25 -1
- package/dist/cli.js +3176 -330
- package/example-argo-estate/README.md +71 -0
- package/example-argo-estate/app-a/chant.config.ts +18 -0
- package/example-argo-estate/app-a/manifests/app.yaml +54 -0
- package/example-argo-estate/app-a/package.json +13 -0
- package/example-argo-estate/app-a/src/app.ts +35 -0
- package/example-argo-estate/app-a/tsconfig.json +1 -0
- package/example-argo-estate/app-b/chant.config.ts +18 -0
- package/example-argo-estate/app-b/manifests/app.yaml +54 -0
- package/example-argo-estate/app-b/package.json +13 -0
- package/example-argo-estate/app-b/src/app.ts +34 -0
- package/example-argo-estate/app-b/tsconfig.json +1 -0
- package/example-argo-estate/control-plane/chant.config.ts +29 -0
- package/example-argo-estate/control-plane/package.json +13 -0
- package/example-argo-estate/control-plane/src/argo.ts +69 -0
- package/example-argo-estate/control-plane/tsconfig.json +1 -0
- package/example-argo-estate/package-lock.json +1949 -0
- package/example-argo-estate/package.json +10 -0
- package/example-carve/README.md +194 -0
- package/example-carve/app/chant.config.ts +6 -0
- package/example-carve/app/package-lock.json +1075 -0
- package/example-carve/app/package.json +13 -0
- package/example-carve/app/src/carved.ts +30 -0
- package/example-carve/app/tsconfig.json +1 -0
- package/example-carve/carve-report.json +872 -0
- package/example-carve/legacy-tf/cdn.tf +23 -0
- package/example-carve/legacy-tf/compute.tf +63 -0
- package/example-carve/legacy-tf/floci-override.tf.disabled +61 -0
- package/example-carve/legacy-tf/modules/cdn/main.tf +72 -0
- package/example-carve/legacy-tf/naming.tf +10 -0
- package/example-carve/legacy-tf/network.tf +119 -0
- package/example-carve/legacy-tf/observability.tf +18 -0
- package/example-carve/legacy-tf/outputs.tf +16 -0
- package/example-carve/legacy-tf/storage.tf +33 -0
- package/example-carve/legacy-tf/terraform.tfstate +602 -0
- package/example-carve/legacy-tf/versions.tf +40 -0
- package/example-flux-estate/README.md +22 -12
- package/example-flux-estate/app-a/package.json +2 -2
- package/example-flux-estate/app-a/src/app.ts +2 -1
- package/example-flux-estate/app-b/chant.config.ts +4 -3
- package/example-flux-estate/app-b/package.json +2 -2
- package/example-flux-estate/app-b/src/app.ts +5 -3
- package/example-flux-estate/control-plane/package.json +2 -2
- package/example-flux-estate/control-plane/src/flux.ts +10 -2
- package/example-flux-estate/package-lock.json +17 -17
- package/example-k8s/README.md +16 -2
- package/example-k8s/base/deployment.yaml +21 -0
- package/example-k8s/base/kustomization.yaml +3 -0
- package/example-k8s/base/service.yaml +12 -0
- package/example-k8s/chant.config.ts +10 -0
- package/example-k8s/overlays/dev/kustomization.yaml +6 -0
- package/example-k8s/package-lock.json +18 -18
- package/example-k8s/package.json +3 -3
- package/example-writes/package-lock.json +14 -14
- package/example-writes/package.json +3 -3
- package/package.json +9 -6
- package/web/app.js +1027 -42
- package/web/carve-steps.js +610 -0
- package/web/carve-steps.test.js +233 -0
- package/web/demos.js +71 -0
- package/web/demos.test.js +83 -0
- package/web/icons/cncf/argo.svg +1 -0
- package/web/icons/cncf/flux.svg +1 -0
- package/web/icons/cncf/helm.svg +1 -0
- package/web/icons/k8s/c-role.svg +1 -0
- package/web/icons/k8s/cm.svg +1 -0
- package/web/icons/k8s/crb.svg +1 -0
- package/web/icons/k8s/crd.svg +1 -0
- package/web/icons/k8s/cronjob.svg +1 -0
- package/web/icons/k8s/deploy.svg +1 -0
- package/web/icons/k8s/ds.svg +1 -0
- package/web/icons/k8s/ep.svg +1 -0
- package/web/icons/k8s/group.svg +1 -0
- package/web/icons/k8s/hpa.svg +1 -0
- package/web/icons/k8s/ing.svg +1 -0
- package/web/icons/k8s/job.svg +1 -0
- package/web/icons/k8s/limits.svg +1 -0
- package/web/icons/k8s/netpol.svg +1 -0
- package/web/icons/k8s/ns.svg +1 -0
- package/web/icons/k8s/pod.svg +1 -0
- package/web/icons/k8s/psp.svg +1 -0
- package/web/icons/k8s/pv.svg +1 -0
- package/web/icons/k8s/pvc.svg +1 -0
- package/web/icons/k8s/quota.svg +1 -0
- package/web/icons/k8s/rb.svg +1 -0
- package/web/icons/k8s/role.svg +1 -0
- package/web/icons/k8s/rs.svg +1 -0
- package/web/icons/k8s/sa.svg +1 -0
- package/web/icons/k8s/sc.svg +1 -0
- package/web/icons/k8s/secret.svg +1 -0
- package/web/icons/k8s/sts.svg +1 -0
- package/web/icons/k8s/svc.svg +1 -0
- package/web/icons/k8s/user.svg +1 -0
- package/web/icons/k8s/vol.svg +1 -0
- package/web/index.html +285 -85
- package/web/json-view.js +334 -0
- package/web/json-view.test.js +218 -0
- package/web/layout-store.js +278 -0
- package/web/layout-store.test.js +388 -0
- package/web/panel.js +28 -0
- package/web/theme.js +114 -6
package/web/panel.js
CHANGED
|
@@ -122,6 +122,34 @@ export function setPanelTab(tab, { expand = true } = {}) {
|
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* #254: add a tab the markup doesn't ship — the Carve walkthrough, which only
|
|
127
|
+
* exists when the server is serving one. Mounted at runtime rather than hidden
|
|
128
|
+
* in index.html so no other project grows a dead tab (and so a persisted
|
|
129
|
+
* `tab: "carve"` can never strand someone on an empty section: the tab isn't
|
|
130
|
+
* there to be restored). Returns the section element to render into; calling it
|
|
131
|
+
* twice is a no-op that returns the same one.
|
|
132
|
+
*/
|
|
133
|
+
export function addPanelTab(id, label, title) {
|
|
134
|
+
if (!panel) return null;
|
|
135
|
+
const existing = panel.querySelector(`#panel-body section[data-tab="${id}"]`);
|
|
136
|
+
if (existing) return existing;
|
|
137
|
+
const b = document.createElement("button");
|
|
138
|
+
b.dataset.tab = id;
|
|
139
|
+
b.textContent = label;
|
|
140
|
+
if (title) b.title = title;
|
|
141
|
+
b.addEventListener("click", () => setPanelTab(id));
|
|
142
|
+
// Before the ⌘K pill + collapse chevron, which are not tabs.
|
|
143
|
+
document.getElementById("panel-tabs").insertBefore(b, document.getElementById("hintk"));
|
|
144
|
+
const section = document.createElement("section");
|
|
145
|
+
section.dataset.tab = id;
|
|
146
|
+
document.getElementById("panel-body").appendChild(section);
|
|
147
|
+
// The new button starts inactive; the section starts hidden. Re-applying the
|
|
148
|
+
// current tab keeps the two in step whatever it happens to be.
|
|
149
|
+
setPanelTab(state.tab, { expand: false });
|
|
150
|
+
return section;
|
|
151
|
+
}
|
|
152
|
+
|
|
125
153
|
function setCollapsed(on) {
|
|
126
154
|
state.collapsed = !!on;
|
|
127
155
|
panel.classList.toggle("collapsed", state.collapsed);
|
package/web/theme.js
CHANGED
|
@@ -32,6 +32,18 @@ export function readableOn(bg) { return isDark(bg) ? "#ffffff" : "#000000"; }
|
|
|
32
32
|
// --- OKLCH (perceptual hue rotation for category counts > palette slots) --
|
|
33
33
|
const s2l = (c) => ((c /= 255) <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
|
|
34
34
|
const l2s = (c) => { const v = c <= 0.0031308 ? c * 12.92 : 1.055 * c ** (1 / 2.4) - 0.055; return clamp(v, 0, 1) * 255; };
|
|
35
|
+
// A real WCAG contrast ratio (#240) — sRGB→linear via s2l (the same EOTF step
|
|
36
|
+
// hexToOklch already does below), unlike the quick luminance() above, which
|
|
37
|
+
// skips gamma correction and is only good enough for a light/dark coin-flip
|
|
38
|
+
// (isDark/readableOn). The floor --muted has to clear is a real accessibility
|
|
39
|
+
// number, and luminance()'s shortcut proved too far off it on the exact
|
|
40
|
+
// palette that opened this issue (Darkermatrix: quick metric said 3.03,
|
|
41
|
+
// actual WCAG was 2.13) to be worth the savings.
|
|
42
|
+
function relLuminance(hex) { const [r, g, b] = hexToRgb(hex).map(s2l); return 0.2126 * r + 0.7152 * g + 0.0722 * b; }
|
|
43
|
+
export function contrast(a, b) {
|
|
44
|
+
const A = relLuminance(a), B = relLuminance(b);
|
|
45
|
+
return (Math.max(A, B) + 0.05) / (Math.min(A, B) + 0.05);
|
|
46
|
+
}
|
|
35
47
|
export function hexToOklch(hex) {
|
|
36
48
|
const [R, G, B] = hexToRgb(hex).map(s2l);
|
|
37
49
|
const l = Math.cbrt(0.4122214708 * R + 0.5363325363 * G + 0.0514459929 * B);
|
|
@@ -53,20 +65,113 @@ export function oklchToHex({ L, C, H }) {
|
|
|
53
65
|
l2s(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s));
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
// Drop a colour's chroma without touching its lightness — the chrome's one
|
|
69
|
+
// "quieter" step (#229). Used on the tokens that SURROUND the drift hues, never
|
|
70
|
+
// on the drift hues themselves.
|
|
71
|
+
export function desaturate(hex, k) {
|
|
72
|
+
const { L, C, H } = hexToOklch(hex);
|
|
73
|
+
return oklchToHex({ L, C: C * k, H });
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// #240: the same guaranteed-visible trick #229 used for --active (push OKLCH
|
|
77
|
+
// L away from a reference until it clears a bar), generalised from a fixed L
|
|
78
|
+
// delta to an actual contrast() floor — because unlike --active's "just don't
|
|
79
|
+
// land on the same fill", secondary text has a real legibility target.
|
|
80
|
+
//
|
|
81
|
+
// Tries `dark`'s intended direction first (lighter text on a dark theme,
|
|
82
|
+
// darker on a light one — the direction a person would actually pick, and
|
|
83
|
+
// the one #229's --active push uses). luminance() isn't gamma-corrected, so
|
|
84
|
+
// its contrast() ratio is asymmetric in a way real perceived contrast isn't
|
|
85
|
+
// — it overstates how much headroom the near-black end has. Chasing that
|
|
86
|
+
// headroom blindly is how a dark theme's muted text ends up pushed to
|
|
87
|
+
// near-black-on-dark-panel, technically clearing the ratio while reading as
|
|
88
|
+
// invisible. So the "chase more headroom" fallback only fires if the
|
|
89
|
+
// intended direction's own gamut-limited walk can't reach the floor at all
|
|
90
|
+
// (a saturated, nominally-dark bg like Hot Dog Stand's red, whose panel ends
|
|
91
|
+
// up mid-bright once mixed toward fg) — never as a first choice. Chroma
|
|
92
|
+
// eases toward 0 as L walks away so a saturated start doesn't clip out of
|
|
93
|
+
// gamut and stall short of the target.
|
|
94
|
+
function pushForContrast(hex, against, floor, dark) {
|
|
95
|
+
if (contrast(hex, against) >= floor) return hex;
|
|
96
|
+
const tok = hexToOklch(hex);
|
|
97
|
+
const walk = (dir) => {
|
|
98
|
+
let best = hex, bestRatio = contrast(hex, against);
|
|
99
|
+
for (let step = 1; step <= 100; step++) {
|
|
100
|
+
const t = step / 100;
|
|
101
|
+
const L = clamp(tok.L + dir * step * 0.01, 0, 1);
|
|
102
|
+
const out = oklchToHex({ L, C: tok.C * (1 - t * 0.6), H: tok.H });
|
|
103
|
+
const ratio = contrast(out, against);
|
|
104
|
+
if (ratio > bestRatio) { bestRatio = ratio; best = out; }
|
|
105
|
+
if (ratio >= floor) return { hex: out, ratio, reached: true };
|
|
106
|
+
if (L === 0 || L === 1) break;
|
|
107
|
+
}
|
|
108
|
+
return { hex: best, ratio: bestRatio, reached: false };
|
|
109
|
+
};
|
|
110
|
+
const primary = walk(dark ? 1 : -1);
|
|
111
|
+
if (primary.reached) return primary.hex;
|
|
112
|
+
const fallback = walk(dark ? -1 : 1);
|
|
113
|
+
return fallback.ratio > primary.ratio ? fallback.hex : primary.hex;
|
|
114
|
+
}
|
|
115
|
+
|
|
56
116
|
// --- behold's semantic tokens derived from a theme -----------------------
|
|
57
117
|
// Drift states are ANCHORED to palette slots so "coloured by drift" stays meaningful across
|
|
58
118
|
// every theme (green=managed … red=degraded); only the shades change. Chrome is derived from
|
|
59
119
|
// bg/fg mixes. `cat` are the chromatic slots that colorForCategory cycles for substrates.
|
|
120
|
+
//
|
|
121
|
+
// #229: the chrome tokens (line/muted/rule/focus/active/well/shadow) are one
|
|
122
|
+
// step quieter than the palette that feeds them — the status hues are behold's
|
|
123
|
+
// actual message, so everything around them gives up chroma to let them land.
|
|
124
|
+
// Every one of them is still DERIVED: no chrome constant is a fixed colour, and
|
|
125
|
+
// all 552 palettes have to come out of here legible.
|
|
60
126
|
export function tokensFor(th) {
|
|
61
127
|
const fgMix = (t) => mix(th.bg, th.fg, t);
|
|
128
|
+
const P = th.palette;
|
|
129
|
+
const panel = fgMix(0.09);
|
|
130
|
+
// The active-control fill (tab strip, the picked option) — the panel pulled
|
|
131
|
+
// toward the accent, then pushed AWAY from the panel's own lightness so the
|
|
132
|
+
// fill is visible even on a palette whose blue slot happens to sit exactly at
|
|
133
|
+
// the panel's lightness (Red Sands, CGA). It stays within one step of the
|
|
134
|
+
// panel, so `--fg` reads on the fill exactly as it reads on the panel — no
|
|
135
|
+
// second ink token to keep legible across the corpus.
|
|
136
|
+
const panelL = hexToOklch(panel).L;
|
|
137
|
+
const rawActive = hexToOklch(mix(panel, P[4], 0.17));
|
|
138
|
+
const active = oklchToHex({
|
|
139
|
+
...rawActive,
|
|
140
|
+
L: clamp(panelL + (th.dark ? 1 : -1) * clamp(Math.abs(rawActive.L - panelL), 0.055, 0.1), 0.05, 0.95),
|
|
141
|
+
});
|
|
62
142
|
return {
|
|
63
143
|
dark: th.dark,
|
|
64
144
|
bg: th.bg,
|
|
65
|
-
panel
|
|
66
|
-
line: fgMix(0.16),
|
|
145
|
+
panel,
|
|
146
|
+
line: desaturate(fgMix(0.16), 0.75),
|
|
67
147
|
fg: th.fg,
|
|
68
|
-
|
|
148
|
+
// #240: secondary text needs an actual floor, not just "quieter than
|
|
149
|
+
// fg" — a handful of low-range palettes (Darkermatrix and friends) mix
|
|
150
|
+
// fg/panel close enough that the quieted 0.5 mix reads at ~1.25:1.
|
|
151
|
+
// Push its OKLCH L away from --panel's until it clears 3:1.
|
|
152
|
+
muted: pushForContrast(desaturate(fgMix(0.5), 0.75), panel, 3, th.dark),
|
|
69
153
|
edge: fgMix(0.32),
|
|
154
|
+
// The structural border — the panel shell, the pane edge, the footer rule.
|
|
155
|
+
// Tinted by the palette's OWN bright-black slot instead of being one more
|
|
156
|
+
// fg/bg mix, but anchored to a guaranteed-visible mix so a theme whose
|
|
157
|
+
// slot 8 sits on the panel's lightness still draws an edge.
|
|
158
|
+
rule: mix(fgMix(0.22), P[8], 0.45),
|
|
159
|
+
// Interaction chrome (hover/focus borders, the load spinner): the accent
|
|
160
|
+
// with most of its chroma taken off, so hover doesn't compete with a
|
|
161
|
+
// pending node for the same blue.
|
|
162
|
+
focus: desaturate(P[4], 0.4),
|
|
163
|
+
active,
|
|
164
|
+
// The recessed ground — the nowline log, the ⌘K input. One perceptual step
|
|
165
|
+
// off the background (down, or UP on the near-black palettes where there is
|
|
166
|
+
// no "darker" left), keeping the bg's own hue so it never reads as a patch
|
|
167
|
+
// of a different theme.
|
|
168
|
+
well: (() => {
|
|
169
|
+
const b = hexToOklch(th.bg);
|
|
170
|
+
return oklchToHex({ ...b, L: clamp(b.L + (b.L < 0.16 ? 0.06 : -0.06), 0, 1) });
|
|
171
|
+
})(),
|
|
172
|
+
// Shadow/scrim base, used through color-mix() at the call site — a light
|
|
173
|
+
// theme's drop shadow must not be the same slab of black a dark one wants.
|
|
174
|
+
shadow: mix(th.bg, "#000000", th.dark ? 0.85 : 0.6),
|
|
70
175
|
managed: th.palette[2], // green — good / managed
|
|
71
176
|
foreign: th.palette[3], // yellow — warn / foreign
|
|
72
177
|
pending: th.palette[4], // blue — accent / pending
|
|
@@ -84,8 +189,10 @@ export function tokensFor(th) {
|
|
|
84
189
|
};
|
|
85
190
|
}
|
|
86
191
|
|
|
87
|
-
// CSS custom properties behold's chrome (:root) reads.
|
|
88
|
-
|
|
192
|
+
// CSS custom properties behold's chrome (:root) reads. The pre-paint fallback
|
|
193
|
+
// block in index.html mirrors these for the DEFAULT theme — smoke/ui-smoke.mjs
|
|
194
|
+
// recomputes it from here and fails if the two ever drift apart.
|
|
195
|
+
const CSS_VARS = ["bg", "panel", "line", "fg", "muted", "edge", "managed", "foreign", "pending", "degraded", "neutral", "runtime", "rule", "focus", "active", "well", "shadow"];
|
|
89
196
|
|
|
90
197
|
// pinhole (the graph painter) reads its OWN vars, `--pin-<token>` (emitted as
|
|
91
198
|
// `var(--pin-token, <baked default>)`). Derive them from the active Ghostty theme so the graph
|
|
@@ -171,7 +278,8 @@ export function mountThemePicker(container) {
|
|
|
171
278
|
const sel = document.createElement("select");
|
|
172
279
|
sel.setAttribute("aria-label", "Color theme");
|
|
173
280
|
sel.title = "Color theme";
|
|
174
|
-
sel.style.cssText =
|
|
281
|
+
sel.style.cssText =
|
|
282
|
+
"background:var(--well);color:var(--fg);border:1px solid var(--line);border-radius:var(--r-ctl);padding:4px 8px;font:var(--t-body)/1.3 var(--font-ui);max-width:220px";
|
|
175
283
|
const groups = { Dark: document.createElement("optgroup"), Light: document.createElement("optgroup") };
|
|
176
284
|
groups.Dark.label = "Dark"; groups.Light.label = "Light";
|
|
177
285
|
for (const name of listThemes()) {
|