@promptctl/cc-candybar 1.7.2 → 1.8.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 +78 -77
- package/package.json +7 -7
- package/src/config/dsl-loader.ts +8 -0
- package/src/config/loader/layout.ts +6 -1
- package/src/config/loader/menu-synth.ts +156 -0
- package/src/config/menu-keys.ts +101 -0
- package/src/dsl/node-registry.ts +59 -11
- package/src/dsl/render.ts +61 -0
- package/src/render/menu.ts +120 -0
- package/src/render/picker.ts +5 -1
- package/src/template-engine/funcs.ts +24 -35
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// [LAW:locality-or-seam] The runtime half of the `{{ menu }}` seam — sibling to
|
|
2
|
+
// `{{ action }}`/`{{ picker }}`. A menu is a self-contained disclosure: an inline
|
|
3
|
+
// glyph that toggles open/closed, and (when open) its body — a picker grid —
|
|
4
|
+
// dropped onto the next line. It composes the two existing helpers, adding only
|
|
5
|
+
// the disclosure: the glyph is realized through the synthesized cycle action
|
|
6
|
+
// (`renderAction`), the body through the one picker renderer (`renderPicker`).
|
|
7
|
+
//
|
|
8
|
+
// [LAW:one-source-of-truth] A menu is CONTEXT-FREE in the template but its
|
|
9
|
+
// accordion identity (which row key, which member name) is a fact about WHERE its
|
|
10
|
+
// host segment sits — derived once in menu-keys and published into this runtime
|
|
11
|
+
// by the render walk before each segment's template evaluates. The helper reads
|
|
12
|
+
// that placement; it never invents a key from its own argument string, so the
|
|
13
|
+
// rendered toggle and the loader-synthesized state var + gate share one source.
|
|
14
|
+
//
|
|
15
|
+
// [LAW:dataflow-not-control-flow] Openness is the value of the row key, not a
|
|
16
|
+
// when-gated reveal: open ⇔ the row key holds THIS menu's member name. Open is a
|
|
17
|
+
// length-1 drop appended after a "\n"; closed is just the glyph. One expression,
|
|
18
|
+
// one returned RichText (the "\n" splits into a dropped line downstream).
|
|
19
|
+
|
|
20
|
+
import { RichText } from "@promptctl/rich-js";
|
|
21
|
+
import type { FuncMap } from "@promptctl/go-template-js";
|
|
22
|
+
import {
|
|
23
|
+
MENU_GLYPH_CLOSED,
|
|
24
|
+
MENU_GLYPH_OPEN,
|
|
25
|
+
menuActionName,
|
|
26
|
+
menuStateKey,
|
|
27
|
+
} from "../config/menu-keys.js";
|
|
28
|
+
import { readVar, renderAction, type ActionRuntime } from "./action.js";
|
|
29
|
+
import { renderPicker } from "./picker.js";
|
|
30
|
+
|
|
31
|
+
// [LAW:types-are-the-program] One menu placement: the structural facts a context-
|
|
32
|
+
// free `{{ menu }}` cannot see about itself. Published by the walk per segment
|
|
33
|
+
// render; the helper reads the live value.
|
|
34
|
+
export interface MenuPlacement {
|
|
35
|
+
readonly rowKey: string;
|
|
36
|
+
readonly segName: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// [LAW:locality-or-seam] The runtime the `menu` func closes over. It shares the
|
|
40
|
+
// ACTION runtime (the menu's glyph and body resolve their actions/state from the
|
|
41
|
+
// same compiled table + store as every other helper) and reads the walk-published
|
|
42
|
+
// current placement. `current` is mutated by the single owner (the render walk,
|
|
43
|
+
// set immediately before each segment template eval) — the spatial cousin of the
|
|
44
|
+
// hue cursor, one mutator, never ambient. [LAW:no-ambient-temporal-coupling]
|
|
45
|
+
export interface MenuRuntime {
|
|
46
|
+
readonly action: ActionRuntime;
|
|
47
|
+
current: MenuPlacement | null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Realize a `{{ menu }}` against the live placement + state into ONE RichText.
|
|
51
|
+
function renderMenu(
|
|
52
|
+
applyName: string,
|
|
53
|
+
pageName: string,
|
|
54
|
+
closeOnPick: boolean,
|
|
55
|
+
paged: boolean,
|
|
56
|
+
runtime: MenuRuntime,
|
|
57
|
+
): RichText {
|
|
58
|
+
const placement = runtime.current;
|
|
59
|
+
// [LAW:no-defensive-null-guards] The walk publishes a placement before every
|
|
60
|
+
// segment template evaluates; a `{{ menu }}` only renders inside a segment. A
|
|
61
|
+
// null here is a wiring bug (the func fired with no current segment), surfaced
|
|
62
|
+
// loudly rather than rendering a placeless menu.
|
|
63
|
+
if (placement === null) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"{{ menu }} rendered with no active segment placement — the render walk must publish one before evaluating a segment template",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const { rowKey, segName } = placement;
|
|
69
|
+
const action = runtime.action;
|
|
70
|
+
|
|
71
|
+
// [LAW:single-enforcer] The disclosure glyph IS the synthesized cycle action —
|
|
72
|
+
// displays bound one-per-member (closed ▸ / open ▾), the current member's glyph
|
|
73
|
+
// renders, the click writes the successor. Same toggle as group sugar.
|
|
74
|
+
const glyph = renderAction(
|
|
75
|
+
menuActionName(rowKey, segName),
|
|
76
|
+
[MENU_GLYPH_CLOSED, MENU_GLYPH_OPEN],
|
|
77
|
+
action,
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
// [LAW:dataflow-not-control-flow] Open ⇔ the row key holds this menu's member.
|
|
81
|
+
// Closed ⇒ just the glyph; open ⇒ glyph + "\n" + body. The "\n" is the sole
|
|
82
|
+
// vertical sentinel (splitCellsIntoLines drops the body below the row).
|
|
83
|
+
const open = readVar(action.store, menuStateKey(rowKey)) === segName;
|
|
84
|
+
if (!open) return glyph;
|
|
85
|
+
|
|
86
|
+
const body = renderPicker(applyName, pageName, closeOnPick, paged, action);
|
|
87
|
+
const combined = RichText.fromFragments([glyph, new RichText("\n"), body]);
|
|
88
|
+
combined.end = "";
|
|
89
|
+
return combined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// [LAW:dataflow-not-control-flow] One func; the two action NAMES select the
|
|
93
|
+
// body's apply/page effects, the two optional bools are the bounded author
|
|
94
|
+
// choices (closeOnPick, paged) — identical surface to `{{ picker }}`, since the
|
|
95
|
+
// body IS a picker. The disclosure takes no argument: its identity is derived
|
|
96
|
+
// from placement, never authored.
|
|
97
|
+
//
|
|
98
|
+
// [LAW:one-way-deps] Injected into the engine by registerDslConfig as data; the
|
|
99
|
+
// generic engine never imports this module.
|
|
100
|
+
export function menuFuncs(runtime: MenuRuntime): FuncMap {
|
|
101
|
+
return {
|
|
102
|
+
menu: {
|
|
103
|
+
fn: (
|
|
104
|
+
applyName: string,
|
|
105
|
+
pageName: string,
|
|
106
|
+
closeOnPick?: boolean,
|
|
107
|
+
paged?: boolean,
|
|
108
|
+
) =>
|
|
109
|
+
renderMenu(
|
|
110
|
+
applyName,
|
|
111
|
+
pageName,
|
|
112
|
+
closeOnPick === true,
|
|
113
|
+
paged === true,
|
|
114
|
+
runtime,
|
|
115
|
+
),
|
|
116
|
+
argTypes: ["string", "string", "bool", "bool"],
|
|
117
|
+
returnType: "T",
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
package/src/render/picker.ts
CHANGED
|
@@ -124,7 +124,11 @@ function requireKind<K extends CompiledActionDecl["kind"]>(
|
|
|
124
124
|
// click APPLIES its option AND (when closeOnPick) resets the page key to -1 in one
|
|
125
125
|
// atomic set-state — the picker owns the page key, so it derives the close-write
|
|
126
126
|
// rather than the author re-stating the key.
|
|
127
|
-
|
|
127
|
+
// [LAW:single-enforcer] Exported so the `{{ menu }}` helper renders its body
|
|
128
|
+
// through the SAME picker renderer — a menu body IS a picker grid; there is no
|
|
129
|
+
// second grid implementation to drift. The menu adds only the disclosure
|
|
130
|
+
// wrapper, never a parallel picker.
|
|
131
|
+
export function renderPicker(
|
|
128
132
|
applyName: string,
|
|
129
133
|
pageName: string,
|
|
130
134
|
closeOnPick: boolean,
|
|
@@ -34,34 +34,17 @@ import { renderSparkline, parseSeries } from "./sparkline.js";
|
|
|
34
34
|
const THEMES_LIST: readonly string[] = listResolvablePaletteNames();
|
|
35
35
|
const STYLES_LIST: readonly string[] = [...STRIP_STYLES];
|
|
36
36
|
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
function num(v: number | bigint): number {
|
|
49
|
-
if (typeof v === "bigint") {
|
|
50
|
-
if (
|
|
51
|
-
v > BigInt(Number.MAX_SAFE_INTEGER) ||
|
|
52
|
-
v < BigInt(Number.MIN_SAFE_INTEGER)
|
|
53
|
-
) {
|
|
54
|
-
throw new TypeError(
|
|
55
|
-
`Numeric argument ${v}n is outside JS safe-integer range ` +
|
|
56
|
-
`(|v| > Number.MAX_SAFE_INTEGER = ${Number.MAX_SAFE_INTEGER}); ` +
|
|
57
|
-
`Number(v) would lose precision or overflow. ` +
|
|
58
|
-
`Pass a value within ±Number.MAX_SAFE_INTEGER.`,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
return Number(v);
|
|
62
|
-
}
|
|
63
|
-
return v;
|
|
64
|
-
}
|
|
37
|
+
// [LAW:single-enforcer] Numeric validation lives at ONE boundary — the engine's
|
|
38
|
+
// `int`/`float` argType gate (@promptctl/go-template-js), which proves membership
|
|
39
|
+
// and normalizes the carrier to a JS `number` before the func body runs. `int`
|
|
40
|
+
// admits only finite integer-valued numbers + safe-integer bigints (rejecting
|
|
41
|
+
// fractionals and precision-losing/overflowing bigints loudly at the gate);
|
|
42
|
+
// `float` admits any finite number. So a formatter wrapper receives a clean
|
|
43
|
+
// `number` and needs no bigint guard of its own — the prior `num()` helper was a
|
|
44
|
+
// second enforcer of what the gate now owns, removed when the formatters adopted
|
|
45
|
+
// int/float. [LAW:no-silent-failure] the gate's rejection is the loud failure on
|
|
46
|
+
// a precision-losing integer input; this is not weaker than the old runtime
|
|
47
|
+
// check, it is the same guarantee moved to the true boundary.
|
|
65
48
|
|
|
66
49
|
// cc-candybar-specific functions not already covered by sprig or Go builtins.
|
|
67
50
|
// The engine also includes sprigDefaults(), sprigStrings(), and sprigLists()
|
|
@@ -185,11 +168,13 @@ export function formatterFuncs(clock: () => Date = () => new Date()): FuncMap {
|
|
|
185
168
|
// segments (`formatLongTimeRemaining (minutesUntilReset .resetsAt)`) and the
|
|
186
169
|
// cacheTimer warmth countdown (numeric `le` thresholds).
|
|
187
170
|
minutesUntilReset: {
|
|
188
|
-
fn: (epochSeconds: number
|
|
171
|
+
fn: (epochSeconds: number) =>
|
|
189
172
|
Math.round(
|
|
190
|
-
Math.max(0,
|
|
173
|
+
Math.max(0, epochSeconds * 1000 - clock().getTime()) / 60000,
|
|
191
174
|
),
|
|
192
|
-
|
|
175
|
+
// [LAW:types-are-the-program] An epoch is integer-valued; `int` rejects a
|
|
176
|
+
// fractional or precision-losing carrier at the gate.
|
|
177
|
+
argTypes: ["int"],
|
|
193
178
|
},
|
|
194
179
|
|
|
195
180
|
// ─── Locale-grouped integer (context's "50,000") ──────────────────
|
|
@@ -200,8 +185,10 @@ export function formatterFuncs(clock: () => Date = () => new Date()): FuncMap {
|
|
|
200
185
|
// of grouping policy — same parsing/formatting boundary that keeps
|
|
201
186
|
// formatModelName here.
|
|
202
187
|
formatInteger: {
|
|
203
|
-
fn: (n: number
|
|
204
|
-
|
|
188
|
+
fn: (n: number) => formatInteger(n),
|
|
189
|
+
// [LAW:types-are-the-program] Integer grouping is meaningful only for an
|
|
190
|
+
// integer; `int` rejects a fractional/precision-losing carrier at the gate.
|
|
191
|
+
argTypes: ["int"],
|
|
205
192
|
},
|
|
206
193
|
|
|
207
194
|
// ─── Numeric helper (block/weekly's Math.round of pct) ────────────
|
|
@@ -210,8 +197,10 @@ export function formatterFuncs(clock: () => Date = () => new Date()): FuncMap {
|
|
|
210
197
|
// formatters.ts module documents domain-meaningful rules; rounding is
|
|
211
198
|
// not domain-meaningful, so it stays here.
|
|
212
199
|
round: {
|
|
213
|
-
fn: (n: number
|
|
214
|
-
|
|
200
|
+
fn: (n: number) => Math.round(n),
|
|
201
|
+
// [LAW:types-are-the-program] round takes a fractional value (e.g. a
|
|
202
|
+
// percentage) → `float` admits any finite number.
|
|
203
|
+
argTypes: ["float"],
|
|
215
204
|
},
|
|
216
205
|
|
|
217
206
|
// ─── Model-name normalizers (chunk-7 model dsl-pending → dsl-parity) ─
|