@promptctl/cc-candybar 1.26.0 → 1.28.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/dist/index.mjs +86 -85
- package/package.json +6 -6
- package/schema/cc-candybar.schema.json +193 -4
- package/src/check.ts +49 -27
- package/src/click/wire.ts +16 -0
- package/src/config/action.ts +57 -22
- package/src/config/default-dsl-config.ts +424 -55
- package/src/config/dsl-loader.ts +14 -2
- package/src/config/dsl-types.ts +59 -0
- package/src/config/loader/actions.ts +283 -109
- package/src/config/loader/cross-ref.ts +148 -28
- package/src/config/loader/emit-schema.ts +2 -0
- package/src/config/loader/globals.ts +118 -31
- package/src/config/loader/merge.ts +58 -1
- package/src/config/loader/persist-target.ts +32 -0
- package/src/config/loader/presets.ts +107 -0
- package/src/config/option-domain.ts +164 -0
- package/src/config/presets.ts +188 -0
- package/src/daemon/cache/git.ts +1 -1
- package/src/daemon/cache/render.ts +72 -7
- package/src/daemon/config-overrides-store.ts +322 -0
- package/src/daemon/paths.ts +10 -0
- package/src/daemon/render-payload.ts +84 -19
- package/src/daemon/server.ts +68 -55
- package/src/daemon/verbs/config-validators.ts +127 -0
- package/src/daemon/verbs/index.ts +129 -2
- package/src/daemon/verbs/state-validators.ts +98 -586
- package/src/daemon/verbs/validator-registry.ts +457 -0
- package/src/demo/dsl.ts +17 -10
- package/src/dsl/node-registry.ts +54 -39
- package/src/dsl/render.ts +158 -46
- package/src/help-text.ts +3 -3
- package/src/install/index.ts +2 -2
- package/src/render/action.ts +155 -33
- package/src/render/active-segment.ts +78 -0
- package/src/render/menu.ts +16 -11
- package/src/render/picker.ts +51 -13
- package/src/render/segment-color.ts +74 -0
- package/src/segments/git.ts +389 -48
- package/src/template-engine/colors.ts +67 -45
- package/src/template-engine/engine.ts +11 -12
- package/src/themes/index.ts +1 -4
- package/src/themes/palette-resolvers.ts +22 -30
- package/src/themes/policy.ts +37 -16
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
// [LAW:single-enforcer] All per-segment bg/fg resolution flows through
|
|
2
2
|
// resolveSegmentColors. No second path; a second path would silently drift
|
|
3
|
-
// from the
|
|
3
|
+
// from the ordered pipeline this function owns.
|
|
4
|
+
//
|
|
5
|
+
// [LAW:no-ambient-temporal-coupling] That ordering — publish the segment's
|
|
6
|
+
// palette, resolve bg, publish bg, resolve fg, then hand the body its turn —
|
|
7
|
+
// is not incidental execution order that happens to work. It is the phase
|
|
8
|
+
// structure of a segment's color resolution, and this function is its one
|
|
9
|
+
// owner. Each phase's output is *published* into the active-segment record
|
|
10
|
+
// before the next phase runs, so a later template can read an earlier
|
|
11
|
+
// phase's result (`fg: '{{ contrastOn (bgOf) }}'`) and an earlier one
|
|
12
|
+
// cannot read a later one — it gets a message naming the phase instead.
|
|
4
13
|
//
|
|
5
14
|
// [LAW:dataflow-not-control-flow] Steps execute unconditionally; the option
|
|
6
15
|
// values (undefined template = no spec) are what decides the output, not
|
|
@@ -8,60 +17,72 @@
|
|
|
8
17
|
// → fragmentsToStripCells's baseStyle merge is a no-op, cells flow through
|
|
9
18
|
// unchanged.
|
|
10
19
|
|
|
11
|
-
import {
|
|
12
|
-
|
|
20
|
+
import {
|
|
21
|
+
Style,
|
|
22
|
+
ColorSpec,
|
|
23
|
+
resolveColorRef,
|
|
24
|
+
ColorRefError,
|
|
25
|
+
} from "@promptctl/rich-js";
|
|
26
|
+
import type { ColorRgba, Palette } from "@promptctl/rich-js";
|
|
13
27
|
import type { RichText } from "@promptctl/rich-js";
|
|
14
28
|
import type { Template } from "@promptctl/go-template-js";
|
|
29
|
+
import type { ActiveSegmentRef } from "../render/active-segment.js";
|
|
15
30
|
|
|
16
31
|
export class ColorSpecError extends Error {
|
|
17
|
-
constructor(spec: string, role: "bg" | "fg") {
|
|
18
|
-
super(`Invalid ${role} color
|
|
32
|
+
constructor(spec: string, role: "bg" | "fg", detail: string) {
|
|
33
|
+
super(`Invalid ${role} color ${JSON.stringify(spec)}: ${detail}`);
|
|
19
34
|
this.name = "ColorSpecError";
|
|
20
35
|
}
|
|
21
36
|
}
|
|
22
37
|
|
|
23
38
|
/**
|
|
24
|
-
* Resolve
|
|
25
|
-
*
|
|
39
|
+
* Resolve a segment's `bg:` and `fg:` templates into the Style that becomes
|
|
40
|
+
* its baseStyle, publishing each phase's result into `ref` as it goes.
|
|
26
41
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* 4. resolver.resolve(fgSpec, { against: bgColor }) → ColorRgba (auto-contrast).
|
|
32
|
-
* 5. Wrap as Style({ bgcolor, color }).
|
|
42
|
+
* A `bg:`/`fg:` field is a template evaluated to a **color reference** — a
|
|
43
|
+
* palette variable name (`"surface-active"`) or a `#RRGGBB` literal. Since
|
|
44
|
+
* rich-js's `resolveColorRef` accepts both and is idempotent, the plain
|
|
45
|
+
* authoring form and a computed one take the identical path:
|
|
33
46
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
47
|
+
* ```json5
|
|
48
|
+
* bg: "surface-active" // a name, evaluated as itself
|
|
49
|
+
* bg: '{{ darken (color "surface-active") 1 }}' // a computed literal
|
|
50
|
+
* ```
|
|
36
51
|
*
|
|
37
|
-
*
|
|
52
|
+
* There is no "is this a name or a color" branch anywhere — one total
|
|
53
|
+
* function over both. [LAW:dataflow-not-control-flow]
|
|
38
54
|
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* from the same transposed palette and their theme-designed relationship is
|
|
42
|
-
* preserved. [LAW:dataflow-not-control-flow]
|
|
55
|
+
* Undefined template → that color is not set in the returned Style, so cells
|
|
56
|
+
* fall through to their own style (or no color if they have none).
|
|
43
57
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
58
|
+
* Hue rotation and looks are not this function's concern: they live upstream
|
|
59
|
+
* as WHICH palette it is handed, so bg, fg, and the body all resolve from one
|
|
60
|
+
* palette and their theme-designed relationships are preserved.
|
|
47
61
|
*/
|
|
48
62
|
export function resolveSegmentColors(
|
|
49
|
-
|
|
63
|
+
ref: ActiveSegmentRef,
|
|
64
|
+
segName: string,
|
|
65
|
+
palette: Palette,
|
|
50
66
|
bgTemplate: Template<RichText> | undefined,
|
|
51
67
|
fgTemplate: Template<RichText> | undefined,
|
|
52
68
|
scope: object,
|
|
53
69
|
): Style {
|
|
70
|
+
// Phase 0 — the palette is live from here until the walk clears it, so
|
|
71
|
+
// `{{ color … }}` in the bg template, the fg template, and the body all read
|
|
72
|
+
// this one palette. `bg` starts undefined: it is what phase 1 computes.
|
|
73
|
+
const active = { segName, palette, bg: undefined as ColorRgba | undefined };
|
|
74
|
+
ref.current = active;
|
|
75
|
+
|
|
76
|
+
// Phase 1 — background.
|
|
54
77
|
const bgSpec = evalToPlainText(bgTemplate, scope);
|
|
55
78
|
const bgColor =
|
|
56
|
-
bgSpec !== undefined
|
|
57
|
-
? resolveSpec(resolver, bgSpec, undefined, "bg")
|
|
58
|
-
: undefined;
|
|
79
|
+
bgSpec !== undefined ? resolveRef(palette, bgSpec, "bg") : undefined;
|
|
59
80
|
|
|
81
|
+
// Phase 2 — publish it, then foreground, which may now ask about it.
|
|
82
|
+
active.bg = bgColor;
|
|
60
83
|
const fgSpec = evalToPlainText(fgTemplate, scope);
|
|
61
84
|
const fgColor =
|
|
62
|
-
fgSpec !== undefined
|
|
63
|
-
? resolveSpec(resolver, fgSpec, bgColor, "fg")
|
|
64
|
-
: undefined;
|
|
85
|
+
fgSpec !== undefined ? resolveRef(palette, fgSpec, "fg") : undefined;
|
|
65
86
|
|
|
66
87
|
return new Style({
|
|
67
88
|
bgcolor: bgColor !== undefined ? ColorSpec.fromRgba(bgColor) : undefined,
|
|
@@ -82,21 +103,22 @@ function evalToPlainText(
|
|
|
82
103
|
.join("");
|
|
83
104
|
}
|
|
84
105
|
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
// `
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
106
|
+
// [LAW:one-source-of-truth] The same rich-js checkpoint the `{{ color }}`
|
|
107
|
+
// template function crosses, so a reference that works in a body works in a
|
|
108
|
+
// `bg:` field and vice versa. Re-thrown with the field's role attached —
|
|
109
|
+
// rich-js knows the reference failed, only cc-candybar knows it came from a
|
|
110
|
+
// `fg:`. [LAW:no-silent-failure] never a substituted default: a broken color
|
|
111
|
+
// reference is a config bug, and the fix is the config.
|
|
112
|
+
function resolveRef(
|
|
113
|
+
palette: Palette,
|
|
114
|
+
ref: string,
|
|
94
115
|
role: "bg" | "fg",
|
|
95
116
|
): ColorRgba {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
117
|
+
try {
|
|
118
|
+
return resolveColorRef(palette, ref);
|
|
119
|
+
} catch (e) {
|
|
120
|
+
if (e instanceof ColorRefError)
|
|
121
|
+
throw new ColorSpecError(ref, role, e.message);
|
|
122
|
+
throw e;
|
|
123
|
+
}
|
|
102
124
|
}
|
|
@@ -18,8 +18,13 @@
|
|
|
18
18
|
// (int here is shadowed by ccCandybarFuncs' var-system cast below).
|
|
19
19
|
// • sprigDicts: dict, get, set, keys, values, pick, omit, hasKey, merge —
|
|
20
20
|
// `dict` lets a helper take multiple named inputs through its one dot arg.
|
|
21
|
-
// • richTextFuncs: bold, italic,
|
|
22
|
-
//
|
|
21
|
+
// • richTextFuncs: fg, bg, bold, italic, link, style (painting) PLUS the
|
|
22
|
+
// palette-free color math (darken, lighten, mix, contrastOn, readableOn,
|
|
23
|
+
// shiftHue, scaleChroma, scaleLightness, shiftLightness) — all from rich-js.
|
|
24
|
+
// Naming a THEME color (`{{ color "primary" }}`) and asking for the current
|
|
25
|
+
// segment's background (`{{ bgOf }}`) are segment-scoped, so they arrive
|
|
26
|
+
// through `extraFuncs` from the DSL render layer, which is the only place
|
|
27
|
+
// that knows which segment is rendering.
|
|
23
28
|
// • ccCandybarFuncs: basename, dirname, int, string, bool, urlEncode,
|
|
24
29
|
// themes, styles, sparkline.
|
|
25
30
|
// • formatterFuncs: minutesUntilReset (clock-reading numeric primitive),
|
|
@@ -39,29 +44,24 @@ import {
|
|
|
39
44
|
sprigConversions,
|
|
40
45
|
sprigDicts,
|
|
41
46
|
} from "@promptctl/go-template-js";
|
|
42
|
-
import type { PaletteResolver } from "@promptctl/rich-js";
|
|
43
47
|
import { richTextFuncs, RichText } from "@promptctl/rich-js";
|
|
44
|
-
import { paletteFuncs } from "@promptctl/rich-js/template-bindings";
|
|
45
48
|
import { ccCandybarFuncs, formatterFuncs } from "./funcs.js";
|
|
46
49
|
|
|
47
50
|
// [LAW:single-enforcer] fromString/toString are declared once here.
|
|
48
|
-
// richTextFuncs() provides
|
|
49
|
-
//
|
|
50
|
-
// resolver is provided — same engine instance, no second parse path.
|
|
51
|
+
// richTextFuncs() provides the theme-independent vocabulary: painting, color
|
|
52
|
+
// math, attributes, links. It needs no configuration, so it is unconditional.
|
|
51
53
|
// [LAW:one-way-deps] `extraFuncs` is an INJECTED FuncMap (e.g. the action +
|
|
52
54
|
// picker feature funcs, built in render/action.ts + render/picker.ts). The
|
|
53
55
|
// generic engine never imports a specific feature — the caller hands it the
|
|
54
56
|
// capability as data, so the dependency runs caller → engine, never engine → feature.
|
|
55
|
-
// [LAW:one-type-per-behavior]
|
|
56
|
-
// one
|
|
57
|
-
// registered.
|
|
57
|
+
// [LAW:one-type-per-behavior] extraFuncs? is a value, not a mode — one factory,
|
|
58
|
+
// one engine shape; the data (its presence) governs what's registered.
|
|
58
59
|
// [LAW:single-enforcer] `clock` is the one time source. It feeds sprigDatetime
|
|
59
60
|
// (the funcs that read "now") AND createEngine's clock option, so every
|
|
60
61
|
// time-dependent evaluation in this engine reads from one seam. Defaulted here
|
|
61
62
|
// so the default literal `() => new Date()` lives in exactly one place; callers
|
|
62
63
|
// that omit it (and forwarders passing `undefined`) inherit it unchanged.
|
|
63
64
|
export function createCcCandybarEngine(
|
|
64
|
-
resolver?: PaletteResolver,
|
|
65
65
|
extraFuncs?: FuncMap,
|
|
66
66
|
clock: () => Date = () => new Date(),
|
|
67
67
|
): Engine<RichText> {
|
|
@@ -88,7 +88,6 @@ export function createCcCandybarEngine(
|
|
|
88
88
|
// decoupled from any payload's nesting — no per-payload helper variant.
|
|
89
89
|
...sprigDicts(),
|
|
90
90
|
...richTextFuncs(),
|
|
91
|
-
...(resolver !== undefined ? paletteFuncs(resolver) : {}),
|
|
92
91
|
// Domain-specific overrides last (wins on collision with sprig aliases).
|
|
93
92
|
// [LAW:one-source-of-truth] ccCandybarFuncs' `int` is the var-system cast
|
|
94
93
|
// (toNumber over VarValue); it intentionally shadows sprigConversions' `int`
|
package/src/themes/index.ts
CHANGED
|
@@ -15,7 +15,4 @@ export {
|
|
|
15
15
|
} from "./policy.js";
|
|
16
16
|
export type { StripStyle } from "./policy.js";
|
|
17
17
|
|
|
18
|
-
export {
|
|
19
|
-
resolverForThemeName,
|
|
20
|
-
transposedResolver,
|
|
21
|
-
} from "./palette-resolvers.js";
|
|
18
|
+
export { paletteForThemeName, transposedPalette } from "./palette-resolvers.js";
|
|
@@ -1,36 +1,32 @@
|
|
|
1
|
-
// Memoized
|
|
1
|
+
// Memoized Palette construction over rich-js. cc-candybar moves theme
|
|
2
2
|
// NAMES and ThemeKey axes (data); rich-js owns every color value operation. Two
|
|
3
|
-
// memos live here: a theme name -> base
|
|
4
|
-
// transposed
|
|
3
|
+
// memos live here: a theme name -> base palette, and a (base, ThemeKey) ->
|
|
4
|
+
// transposed palette. They compose — the per-render base palette feeds the
|
|
5
5
|
// per-segment transposition (the session look's axes + the segment's hue shift,
|
|
6
6
|
// folded into one key by the caller).
|
|
7
7
|
//
|
|
8
8
|
// [LAW:no-shared-mutable-globals] Single owner: this module. Both Maps are pure
|
|
9
9
|
// memos of pure rich-js functions, keyed by immutable inputs (resolved theme
|
|
10
10
|
// name; palette name + the four ThemeKey axes). rich-js palettes are immutable
|
|
11
|
-
// registry singletons, so a cached
|
|
11
|
+
// registry singletons, so a cached palette never goes stale. Key spaces are
|
|
12
12
|
// bounded by #themes and #themes × #declared looks × #distinct hueShifts
|
|
13
13
|
// (hueShift = look shift + segIndex*hueStep, segIndex bounded by layout; look
|
|
14
14
|
// axes bounded by the loaded configs' looks blocks) — both small. Shared on
|
|
15
|
-
// purpose: a theme's base
|
|
15
|
+
// purpose: a theme's base palette and its gruvbox+42° transposition are each
|
|
16
16
|
// computed once per process, not once per RenderCache entry or per render.
|
|
17
17
|
// Read/written only through the two functions below.
|
|
18
18
|
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
transposePalette,
|
|
22
|
-
getThemePalette,
|
|
23
|
-
} from "@promptctl/rich-js";
|
|
24
|
-
import type { ThemeKey } from "@promptctl/rich-js";
|
|
19
|
+
import { transposePalette, getThemePalette } from "@promptctl/rich-js";
|
|
20
|
+
import type { ThemeKey, Palette } from "@promptctl/rich-js";
|
|
25
21
|
import { resolvePaletteName } from "./policy.js";
|
|
26
22
|
|
|
27
|
-
const baseCache = new Map<string,
|
|
28
|
-
const transposeCache = new Map<string,
|
|
23
|
+
const baseCache = new Map<string, Palette>();
|
|
24
|
+
const transposeCache = new Map<string, Palette>();
|
|
29
25
|
|
|
30
26
|
/**
|
|
31
|
-
* The
|
|
27
|
+
* The Palette for a theme name (aliases resolved). Memoized.
|
|
32
28
|
*
|
|
33
|
-
* [LAW:single-enforcer] The one place a theme name becomes a
|
|
29
|
+
* [LAW:single-enforcer] The one place a theme name becomes a Palette —
|
|
34
30
|
* the per-render base palette and per-segment `palette:` overrides both flow
|
|
35
31
|
* through here. A name that does not resolve is registry/resolver drift, never
|
|
36
32
|
* user error: the loader validates `globals.palette` and the set-state verb
|
|
@@ -38,7 +34,7 @@ const transposeCache = new Map<string, PaletteResolver>();
|
|
|
38
34
|
* name reaches here it must resolve. [LAW:no-defensive-null-guards] the throw is
|
|
39
35
|
* the loud failure for that broken invariant, not a fallback.
|
|
40
36
|
*/
|
|
41
|
-
export function
|
|
37
|
+
export function paletteForThemeName(name: string): Palette {
|
|
42
38
|
const resolved = resolvePaletteName(name);
|
|
43
39
|
const hit = baseCache.get(resolved);
|
|
44
40
|
if (hit !== undefined) return hit;
|
|
@@ -50,14 +46,13 @@ export function resolverForThemeName(name: string): PaletteResolver {
|
|
|
50
46
|
`theme registry — allowed names and the registry are inconsistent`,
|
|
51
47
|
);
|
|
52
48
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return resolver;
|
|
49
|
+
baseCache.set(resolved, palette);
|
|
50
|
+
return palette;
|
|
56
51
|
}
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* `base` transposed by a full ThemeKey — the adapted-palette constructor:
|
|
55
|
+
* (base palette, key) → palette. The caller
|
|
61
56
|
* composes whatever axes it carries (a look's four axes, the per-segment hue
|
|
62
57
|
* shift) into ONE key and this makes ONE transposePalette call — never chain
|
|
63
58
|
* two transpositions: chaining double-pays OKLCH quantization AND collides this
|
|
@@ -70,23 +65,20 @@ export function resolverForThemeName(name: string): PaletteResolver {
|
|
|
70
65
|
* (error/success/warning), so semantic meaning is preserved by construction —
|
|
71
66
|
* no local exemption list to drift.
|
|
72
67
|
*
|
|
73
|
-
* [LAW:single-enforcer] The sole place a transposed
|
|
68
|
+
* [LAW:single-enforcer] The sole place a transposed palette is built — a
|
|
74
69
|
* future look `roles` remap is additive at this one seam. The memo miss
|
|
75
70
|
* (undefined) is genuine optionality — not-yet-computed — not a defended
|
|
76
71
|
* invariant. [LAW:one-source-of-truth] The cache key carries every axis of the
|
|
77
72
|
* ThemeKey: two keys differing on any axis are distinct palettes.
|
|
78
73
|
*/
|
|
79
|
-
export function
|
|
80
|
-
base: PaletteResolver,
|
|
81
|
-
key: ThemeKey,
|
|
82
|
-
): PaletteResolver {
|
|
74
|
+
export function transposedPalette(base: Palette, key: ThemeKey): Palette {
|
|
83
75
|
const cacheKey =
|
|
84
|
-
`${base.
|
|
76
|
+
`${base.name} ${key.hueShift} ${key.chromaScale} ` +
|
|
85
77
|
`${key.lightnessScale} ${key.lightnessShift}`;
|
|
86
78
|
const hit = transposeCache.get(cacheKey);
|
|
87
79
|
if (hit !== undefined) return hit;
|
|
88
80
|
|
|
89
|
-
const
|
|
90
|
-
transposeCache.set(cacheKey,
|
|
91
|
-
return
|
|
81
|
+
const transposed = transposePalette(base, key);
|
|
82
|
+
transposeCache.set(cacheKey, transposed);
|
|
83
|
+
return transposed;
|
|
92
84
|
}
|
package/src/themes/policy.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// theme registry. cc-candybar selects theme NAMES and style IDENTIFIERS here;
|
|
3
3
|
// every color *value* operation (hydrate hex, resolve specs, darken/contrast,
|
|
4
4
|
// hue/transpose) lives in rich-js. This module only moves strings — no
|
|
5
|
-
//
|
|
5
|
+
// Palette, no ColorRgba, no hex. The semantic/anchor knowledge
|
|
6
6
|
// (which tokens keep their hue) stays in rich-js (ANCHORED_ROOTS), not here.
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -51,28 +51,49 @@ export function listResolvablePaletteNames(): readonly string[] {
|
|
|
51
51
|
return [...listThemePalettes(), ...listThemeAliases()];
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// --- Per-config member selection ---
|
|
55
|
+
|
|
56
|
+
// [LAW:one-type-per-behavior] THE resolution shape shared by every selection
|
|
57
|
+
// whose domain is PER-CONFIG (declared in the config, not a registry-static
|
|
58
|
+
// list): session choice over config default over a floor, then collapse to the
|
|
59
|
+
// floor if the chosen name is not a declared member. `looks` and `presets` are
|
|
60
|
+
// both exactly this — what differs between them is only the floor name and
|
|
61
|
+
// which map holds the members, i.e. DATA. Written once here rather than twice,
|
|
62
|
+
// so a change to the collapse rule cannot land on one and miss the other.
|
|
63
|
+
//
|
|
64
|
+
// [LAW:dataflow-not-control-flow] The `??` chain IS the precedence — no "if the
|
|
65
|
+
// session has one" branch anywhere. [LAW:no-silent-failure] the collapse is the
|
|
66
|
+
// visible floor, not a throw: a stale SessionState entry from a prior config's
|
|
67
|
+
// vocabulary (config edits can orphan a name the per-config gate once admitted)
|
|
68
|
+
// must render the floor, and the caller publishes the RESOLVED name onto the
|
|
69
|
+
// payload so a menu label can never claim a member the render did not use.
|
|
70
|
+
//
|
|
71
|
+
// The floor's membership is a load-time guarantee, not a runtime hope: the
|
|
72
|
+
// bundled stdlib ships it and merge-by-name cannot remove it.
|
|
73
|
+
export function effectiveMemberName(
|
|
74
|
+
sessionPick: string | null,
|
|
75
|
+
configDefault: string | undefined,
|
|
76
|
+
floor: string,
|
|
77
|
+
declared: Readonly<Record<string, unknown>>,
|
|
78
|
+
): string {
|
|
79
|
+
const chosen = sessionPick ?? configDefault ?? floor;
|
|
80
|
+
return Object.prototype.hasOwnProperty.call(declared, chosen)
|
|
81
|
+
? chosen
|
|
82
|
+
: floor;
|
|
83
|
+
}
|
|
84
|
+
|
|
54
85
|
// --- Look (theme-adaptation) identifiers ---
|
|
55
86
|
|
|
56
|
-
// The look
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
// domains, the look domain is PER-CONFIG (the merged `looks` block), so the
|
|
61
|
-
// declared names arrive as data. A value outside them (a stale SessionState
|
|
62
|
-
// entry from a prior config's look vocabulary — config edits can orphan a
|
|
63
|
-
// clicked name the per-config gate once admitted) collapses to "none", which
|
|
64
|
-
// every merged DslConfig carries by construction (the bundled stdlib ships it;
|
|
65
|
-
// merge-by-name cannot remove it) — the same collapse-to-floor that keeps
|
|
66
|
-
// effectiveStripStyle's return honest rather than silently widening.
|
|
87
|
+
// [LAW:one-source-of-truth] The look domain's instance of the shared resolver
|
|
88
|
+
// above — the floor is `"none"`, the identity adaptation every merged config
|
|
89
|
+
// carries. A named wrapper (not a bare call at each site) so the floor is
|
|
90
|
+
// spelled once and the three call sites cannot disagree about it.
|
|
67
91
|
export function effectiveLookName(
|
|
68
92
|
sessionLook: string | null,
|
|
69
93
|
globalsLook: string | undefined,
|
|
70
94
|
declaredLooks: Readonly<Record<string, ThemeKey>>,
|
|
71
95
|
): string {
|
|
72
|
-
|
|
73
|
-
return Object.prototype.hasOwnProperty.call(declaredLooks, chosen)
|
|
74
|
-
? chosen
|
|
75
|
-
: "none";
|
|
96
|
+
return effectiveMemberName(sessionLook, globalsLook, "none", declaredLooks);
|
|
76
97
|
}
|
|
77
98
|
|
|
78
99
|
// [LAW:single-enforcer] The one place an effective look NAME becomes the
|