@azure-id/orc 1.4.0 → 1.4.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/bin/cli.js CHANGED
@@ -34405,6 +34405,43 @@ const STATUSLINE_RENDERERS = {
34405
34405
  pulse: { kind: "motif", sub: "pulse", needs: "motif" },
34406
34406
  };
34407
34407
 
34408
+ // WHICH RENDERERS DRAW A LABEL. Seven of thirty-five, and the other
34409
+ // twenty-eight ignore `label` entirely — a bar with a word in front of it is a
34410
+ // different renderer (`plain`), which is the design. That was invisible in the
34411
+ // panel: the Name box was offered on every shape, took the text, wrote it to
34412
+ // disk correctly, and then nothing appeared on the bar. A control that cannot
34413
+ // do anything must SAY so.
34414
+ //
34415
+ // It is a mirror of the switch in `slLowerItem`, and a golden test reads that
34416
+ // switch and compares — the `DIY_STEPS` shape. Adding a `pushLabel` branch
34417
+ // without adding the name here fails the test rather than shipping a control
34418
+ // that lies.
34419
+ const SL_LABEL_RENDERERS = new Set(["plain", "label-value", "bracket", "angle", "badge", "pill", "stack"]);
34420
+
34421
+ // The item fields a renderer CONSUMES. Everything else on the chip is ignored
34422
+ // by that shape, and the panel greys it out with this as the reason.
34423
+ //
34424
+ // The split is the compiler's own: only a `text` (and `link`) renderer lowers
34425
+ // the FORMAT — `case`, `prefix`, `suffix`, `format`, `compact`, `min_width`,
34426
+ // `precision`, `truncate` all ride inside it — while a bar takes `width`,
34427
+ // `ramp` and `threshold`, a series takes `width`, and a state or a motif takes
34428
+ // neither. Colour and emphasis apply everywhere, so they are never listed as
34429
+ // unusable.
34430
+ function slRendererUses(name) {
34431
+ const rend = STATUSLINE_RENDERERS[name];
34432
+ if (!rend) return [];
34433
+ const out = [];
34434
+ if (SL_LABEL_RENDERERS.has(name)) out.push("label");
34435
+ if (rend.kind === "text" || rend.kind === "link")
34436
+ out.push("case", "prefix", "suffix", "format", "compact", "min_width", "precision", "truncate");
34437
+ if (rend.width) out.push("width");
34438
+ if (rend.kind === "bar" || name === "bar") {
34439
+ out.push("ramp");
34440
+ if (!out.includes("width")) out.push("width");
34441
+ }
34442
+ return out;
34443
+ }
34444
+
34408
34445
  // ── The glyph sets (design-language.md §2) ──────────────────────────────────
34409
34446
  // R1: every glyph here is EXACTLY one column in a monospaced terminal, and a
34410
34447
  // test walks all of them. Fullwidth forms, bare emoji and combining marks are
@@ -34533,6 +34570,38 @@ const STATUSLINE_HIDE_WHEN = [
34533
34570
  { id: "wide", says: "hide above this component's max_cols" },
34534
34571
  ];
34535
34572
 
34573
+ // The separators a line can put BETWEEN its parts. A free-text box asked the
34574
+ // user to invent a rhythm and to type a character their keyboard may not have;
34575
+ // this is the closed set, and the panel renders it as a dropdown because the
34576
+ // panel names none of these itself. `value` is what is written into the layout,
34577
+ // `name` is what a person reads. A layout may still carry any string — a value
34578
+ // outside this set is kept and offered back, the way an unset `fixed_executor`
34579
+ // leads its own dropdown.
34580
+ const SL_SEPARATORS = [
34581
+ { value: " · ", name: "middle dot" },
34582
+ { value: " ", name: "one space" },
34583
+ { value: " ", name: "two spaces" },
34584
+ { value: " │ ", name: "upright line" },
34585
+ { value: " — ", name: "long dash" },
34586
+ { value: " - ", name: "hyphen" },
34587
+ { value: " / ", name: "slash" },
34588
+ { value: " • ", name: "bullet" },
34589
+ { value: " ▸ ", name: "small arrow" },
34590
+ { value: " » ", name: "double arrow" },
34591
+ { value: ", ", name: "comma" },
34592
+ { value: " | ", name: "pipe" },
34593
+ ];
34594
+
34595
+ // What each colour set is FOR. The names are the CLI's own and are never
34596
+ // translated; this sentence is why a person would pick one, and without it the
34597
+ // picker is four words with no meaning.
34598
+ const SL_THEME_ABOUT = {
34599
+ terminal: "the default: your terminal's own colours, one accent",
34600
+ dim: "everything quiet — labels and values both greyed",
34601
+ "high-contrast": "the brightest slot of every colour, for a pale terminal",
34602
+ mono: "no colour at all; weight and shape carry every state",
34603
+ };
34604
+
34536
34605
  const SL_FORMATS = ["percent", "ratio", "fraction", "decimal", "plain"];
34537
34606
  const SL_COMPACT = ["off", "si", "bytes"];
34538
34607
  const SL_CASES = ["none", "upper", "lower", "title"];
@@ -34787,6 +34856,15 @@ const SL_BY_ID = new Map(STATUSLINE_COMPONENTS.map((c) => [c.id, c]));
34787
34856
  // compiler, the IR, every renderer, the glyph sets, the colour model and the
34788
34857
  // validator — what differs is the component set, the file names and the config
34789
34858
  // key. A second compiler is exactly what this table exists to prevent.
34859
+ // HOW MUCH ONE LINE MAY SAY (v1.4.2). Six, not five. The limit is about
34860
+ // reading, not about rendering: a line is scanned at a glance, and past about
34861
+ // half a dozen facts the glance becomes a read. Six is where people actually
34862
+ // stopped asking for one more — and it is ONE number, here, because a slot cap
34863
+ // spelled out at each of its five call sites is a cap that drifts on the first
34864
+ // change. A structural part (a spacer, a divider, a fill) is not a thing the
34865
+ // line SAYS and still does not count against it.
34866
+ const SL_MAX_PER_LINE = 6;
34867
+
34790
34868
  const SL_BOARDS = {
34791
34869
  status: {
34792
34870
  id: "status",
@@ -34973,7 +35051,7 @@ function slValidate(layout) {
34973
35051
  E(`line ${i + 1} holds ${rawCounts[i]} component${rawCounts[i] === 1 ? "" : "s"} but line ${i} is empty — fill line ${i} first`);
34974
35052
  }
34975
35053
  layout.lines.forEach((l, i) => {
34976
- if (counts[i] > 5) E(`line ${i + 1} holds ${counts[i]} components (max 5) — remove one`);
35054
+ if (counts[i] > SL_MAX_PER_LINE) E(`line ${i + 1} holds ${counts[i]} components (max ${SL_MAX_PER_LINE}) — remove one`);
34977
35055
  });
34978
35056
 
34979
35057
  const seenIds = new Set();
@@ -35280,6 +35358,13 @@ function slCompile(layout) {
35280
35358
  pad_r: (r.padding && r.padding[1]) || 0,
35281
35359
  children,
35282
35360
  };
35361
+ // WHETHER THIS ONE COUNTS AGAINST THE LINE'S CAP (v1.4.2). The hook's
35362
+ // cheap shape guard has to count the same way the validator does, and it
35363
+ // cannot: the compiled op carries an INSTANCE id, never a component type.
35364
+ // So the compiler — which is the one place that knows — says so. Without
35365
+ // it the guard counted spacers too, and a legal line of six plus a fill
35366
+ // fell back to the shipped lines with no visible reason.
35367
+ if (slIsStructural(item.type)) itemOp.s = 1;
35283
35368
  if (r.min_cols) itemOp.min_cols = r.min_cols;
35284
35369
  if (r.max_cols) itemOp.max_cols = r.max_cols;
35285
35370
  ops.push(itemOp);
@@ -35343,6 +35428,13 @@ function slCompile(layout) {
35343
35428
  providers: [...providers].sort(),
35344
35429
  series: [...seriesWanted].sort(),
35345
35430
  needs_refresh_interval: timeBased ? 5 : null,
35431
+ // THE CAP TRAVELS WITH THE PROGRAM (v1.4.2). A hook cannot call this CLI —
35432
+ // a status line re-renders on every keystroke — so before this it simply
35433
+ // held its own `5`, and raising the cap here left every legal six-part line
35434
+ // falling back to the shipped lines with the reason only in a ledger file.
35435
+ // The lock is already gated on `orc_version`, so a stale copy cannot
35436
+ // outlive the build that wrote it.
35437
+ max_per_line: SL_MAX_PER_LINE,
35346
35438
  warnings: [],
35347
35439
  };
35348
35440
  return { compiled, lock };
@@ -36055,6 +36147,8 @@ function statusline() {
36055
36147
  return slRemoveCmd(claudeDir, p);
36056
36148
  case "line":
36057
36149
  return slLineCmd(claudeDir, p);
36150
+ case "doc":
36151
+ return slDocCmd(claudeDir, p);
36058
36152
  case "presets":
36059
36153
  return slPresetsCmd(claudeDir);
36060
36154
  case "apply":
@@ -36075,6 +36169,7 @@ function statusline() {
36075
36169
  orc statusline move <from-line>:<pos> <to-line>:<pos>
36076
36170
  orc statusline remove <line>:<pos>
36077
36171
  orc statusline line <n> --separator "…" [--max-width N] [--theme T]
36172
+ orc statusline doc [--theme T] [--glyphs G] [--ansi auto|off] [--align-columns on|off]
36078
36173
  orc statusline group <line>:<pos> <line>:<pos> [...] wrap 2-4 as one object
36079
36174
  orc statusline expand <line>:<pos> a composite, or a group, back into its parts
36080
36175
  orc statusline clone <line>:<pos>
@@ -36113,6 +36208,10 @@ function slComponentsCmd() {
36113
36208
  binding: c.binding,
36114
36209
  composite: c.composite || null,
36115
36210
  time_based: !!c.time_based,
36211
+ // Whether this one eats a slot. A spacer is not a thing the line SAYS, so
36212
+ // it does not count against the five — and the panel cannot work that out
36213
+ // for itself without owning a second idea of the catalogue.
36214
+ structural: slIsStructural(c.id),
36116
36215
  previews: slRendererPreviews(c),
36117
36216
  };
36118
36217
  out.push(row);
@@ -36126,7 +36225,12 @@ function slComponentsCmd() {
36126
36225
  count: out.length,
36127
36226
  groups: SL_GROUP_NAMES,
36128
36227
  components: out,
36129
- renderers: Object.fromEntries(Object.entries(STATUSLINE_RENDERERS).map(([k, v]) => [k, { kind: v.kind, form: v.form || null, needs: v.needs, width: v.width || null, decoration: !!v.decoration }])),
36228
+ // `uses` is the field list this shape actually consumes. The panel greys
36229
+ // out the controls it does not, rather than offering a Name box on a bar.
36230
+ renderers: Object.fromEntries(Object.entries(STATUSLINE_RENDERERS).map(([k, v]) => [k, { kind: v.kind, form: v.form || null, needs: v.needs, width: v.width || null, decoration: !!v.decoration, uses: slRendererUses(k) }])),
36231
+ // Which shapes DO draw a label, so a refusal can name them instead of
36232
+ // saying "not here".
36233
+ label_renderers: [...SL_LABEL_RENDERERS],
36130
36234
  glyph_sets: Object.keys(STATUSLINE_GLYPHSETS),
36131
36235
  ramps: Object.fromEntries(Object.entries(STATUSLINE_RAMPS).map(([k, v]) => [k, { stops: v.stops, colors: v.colors, why: v.why }])),
36132
36236
  themes: STATUSLINE_THEMES,
@@ -36138,9 +36242,11 @@ function slComponentsCmd() {
36138
36242
  emphasis: Object.keys(SL_EMPHASIS),
36139
36243
  refused_emphasis: SL_REFUSED_EMPHASIS,
36140
36244
  colors: Object.keys(SL_ANSI_SLOTS),
36245
+ separators: SL_SEPARATORS,
36246
+ themes_about: SL_THEME_ABOUT,
36141
36247
  board: board.id,
36142
36248
  boards: Object.keys(SL_BOARDS),
36143
- max_per_line: 5,
36249
+ max_per_line: SL_MAX_PER_LINE,
36144
36250
  lines: board.lines,
36145
36251
  config_key: board.key,
36146
36252
  setting: board.setting,
@@ -36204,6 +36310,31 @@ function slLoadOrDefault(claudeDir) {
36204
36310
  return slReadLayout(claudeDir, b) || slBuildPreset(b.id === "subagent" ? "agent-default" : "orc-default");
36205
36311
  }
36206
36312
 
36313
+ // EVERY field a chip can carry, in the order the editor asks about them. It is
36314
+ // ONE list: `show --json` emits all of them, and the panel's editor reads them
36315
+ // back. A field that is settable and not listed here is a write with no way to
36316
+ // see it again, which is the v1.4.2 defect this exists to make impossible.
36317
+ const SL_ITEM_FIELDS = [
36318
+ "render", "label", "color", "label_color", "value_color", "bg", "ramp",
36319
+ "glyphs", "format", "case", "truncate", "compact", "prefix", "suffix",
36320
+ "emphasis", "hide_when", "width", "precision", "min_width", "min_cols",
36321
+ "max_cols", "priority", "max_len", "align", "draw_empty", "unknown",
36322
+ ];
36323
+ // The ones whose absence is an empty LIST rather than a null — a control that
36324
+ // iterates a null throws, and a null here would read as "unknown" when it means
36325
+ // "nothing selected".
36326
+ const SL_ITEM_LIST_FIELDS = new Set(["emphasis", "hide_when"]);
36327
+
36328
+ function slAuthoredFields(it) {
36329
+ const out = {};
36330
+ for (const k of Object.keys(it || {})) {
36331
+ if (k === "id" || k === "type" || k === "children") continue;
36332
+ if (it[k] === null || it[k] === undefined) continue;
36333
+ out[k] = it[k];
36334
+ }
36335
+ return out;
36336
+ }
36337
+
36207
36338
  function slShowCmd(claudeDir) {
36208
36339
  const layout = slReadLayout(claudeDir);
36209
36340
  const eff = layout || slBuildPreset("orc-default");
@@ -36221,23 +36352,29 @@ function slShowCmd(claudeDir) {
36221
36352
  max_width: l.max_width || 0,
36222
36353
  count: (l.items || []).length,
36223
36354
  counted: (l.items || []).filter((it) => !slIsStructural(it.type)).length,
36224
- full: (l.items || []).filter((it) => !slIsStructural(it.type)).length >= 5,
36355
+ full: (l.items || []).filter((it) => !slIsStructural(it.type)).length >= SL_MAX_PER_LINE,
36356
+ // `--json is not a summary` (v0.49.1), found once more (v1.4.2). This
36357
+ // emitted TWELVE of the twenty-four fields a chip can carry, so a user who
36358
+ // set `case`, `prefix`, `min_cols` or `precision` got a write that landed on
36359
+ // disk and a panel that could never read it back — the editor reopened with
36360
+ // the control blank and the change looked like it had done nothing. Every
36361
+ // resolved field ships now, and `authored` carries the raw item so a
36362
+ // control can say which values are the USER'S rather than inherited.
36225
36363
  items: (l.items || []).map((it, pi) => {
36226
36364
  const r = slResolveItem(it, eff, l);
36227
- return {
36365
+ const row = {
36228
36366
  pos: pi + 1,
36229
36367
  id: it.id,
36230
36368
  type: it.type,
36231
- render: r ? r.render : it.render,
36232
- label: r ? r.label : null,
36233
- label_color: r ? r.label_color || null : null,
36234
- value_color: r ? r.value_color || null : null,
36235
- ramp: r ? r.ramp || null : null,
36236
- emphasis: r ? r.emphasis || [] : [],
36237
- hide_when: r ? r.hide_when || [] : [],
36238
- unknown: r ? r.unknown : null,
36239
36369
  known: SL_BY_ID.has(it.type),
36370
+ // WHAT THE USER SET, verbatim — never the resolution. A control that
36371
+ // cannot tell "you chose the theme's colour" from "you chose nothing"
36372
+ // cannot offer to clear it.
36373
+ authored: slAuthoredFields(it),
36240
36374
  };
36375
+ for (const k of SL_ITEM_FIELDS) row[k] = r && r[k] !== undefined && r[k] !== null ? r[k] : SL_ITEM_LIST_FIELDS.has(k) ? [] : null;
36376
+ if (it.children) row.children = it.children.map((k) => ({ id: k.id, type: k.type, authored: slAuthoredFields(k) }));
36377
+ return row;
36241
36378
  }),
36242
36379
  }));
36243
36380
  if (wantsJson())
@@ -36262,7 +36399,7 @@ function slShowCmd(claudeDir) {
36262
36399
  console.log("");
36263
36400
  for (const l of lines) {
36264
36401
  const cells = l.items.map((i) => ui.color.cyan(i.type) + ui.color.gray("/" + i.render)).join(ui.color.gray(" · "));
36265
- console.log(` ${ui.color.gray("line " + l.line)} ${cells || ui.color.gray("(empty)")} ${l.full ? ui.color.yellow("5/5") : ui.color.gray(l.counted + "/5")}`);
36402
+ console.log(` ${ui.color.gray("line " + l.line)} ${cells || ui.color.gray("(empty)")} ${l.full ? ui.color.yellow(l.counted + "/" + SL_MAX_PER_LINE) : ui.color.gray(l.counted + "/" + SL_MAX_PER_LINE)}`);
36266
36403
  }
36267
36404
  if (preview) {
36268
36405
  console.log("");
@@ -36318,7 +36455,17 @@ function slValidateCmd(claudeDir) {
36318
36455
  }
36319
36456
 
36320
36457
  function slPreviewCmd(claudeDir) {
36321
- const layout = slLoadOrDefault(claudeDir);
36458
+ const saved = slLoadOrDefault(claudeDir);
36459
+ // A RENDER-ONLY OVERRIDE (v1.4.2). `--theme` and `--glyphs` draw the SAVED
36460
+ // layout under a different colour set or symbol set WITHOUT writing one — so
36461
+ // the panel can show what each choice actually looks like instead of asking
36462
+ // somebody to pick a colour set from its name. It is the same engine and the
36463
+ // same layout; only the two document fields move, on a copy.
36464
+ const layout = JSON.parse(JSON.stringify(saved));
36465
+ const asTheme = flag("--theme");
36466
+ const asGlyphs = flag("--glyphs");
36467
+ if (typeof asTheme === "string" && STATUSLINE_THEMES[asTheme]) layout.theme = asTheme;
36468
+ if (typeof asGlyphs === "string" && STATUSLINE_GLYPHSETS[asGlyphs]) layout.glyphs = asGlyphs;
36322
36469
  const v = slValidate(layout);
36323
36470
  if (!v.ok) {
36324
36471
  if (wantsJson()) return emitJson({ ok: false, reason: "invalid", errors: v.errors }, 1);
@@ -36357,6 +36504,11 @@ function slPreviewCmd(claudeDir) {
36357
36504
  const out = {
36358
36505
  ok: true,
36359
36506
  width: cols,
36507
+ // WHAT THIS PICTURE WAS DRAWN WITH. The panel compares colour sets by
36508
+ // asking for the same layout several times; without these two it could not
36509
+ // label the answers.
36510
+ theme: layout.theme,
36511
+ glyphs: layout.glyphs,
36360
36512
  state,
36361
36513
  fixture: (SL_FIXTURES[state] || SL_FIXTURES.healthy).label,
36362
36514
  fixtures: Object.fromEntries(Object.entries(SL_FIXTURES).map(([k, f]) => [k, f.label])),
@@ -36499,6 +36651,51 @@ function slLineCmd(claudeDir, p) {
36499
36651
  return slSaveAndCompile(claudeDir, layout, { action: "line", line: n });
36500
36652
  }
36501
36653
 
36654
+ // THE DOCUMENT-LEVEL SETTINGS, and they are a separate command on purpose.
36655
+ // `line <n> --theme` sets ONE line's override; the colour set a user picks in
36656
+ // the panel is the WHOLE layout's, and writing it onto line 1 changed one third
36657
+ // of the bar while `show --json` kept reporting the document theme — so the
36658
+ // picker looked dead and was in fact half working, which is worse. One command
36659
+ // per scope, and the panel calls the one that matches the control.
36660
+ function slDocCmd(claudeDir, p) {
36661
+ const layout = slLoadOrDefault(claudeDir);
36662
+ const th = flag("--theme");
36663
+ if (typeof th === "string") {
36664
+ if (!STATUSLINE_THEMES[th]) {
36665
+ if (wantsJson()) return emitJson({ ok: false, reason: "unknown-theme", theme: th, known: Object.keys(STATUSLINE_THEMES) }, 2);
36666
+ console.error(`unknown colour set "${th}" — known: ${Object.keys(STATUSLINE_THEMES).join(", ")}`);
36667
+ process.exit(2);
36668
+ }
36669
+ layout.theme = th;
36670
+ // A per-LINE override outranks the document, so a colour set chosen here
36671
+ // would be invisible on any line that carries one. The overrides are
36672
+ // cleared and the answer NAMES the lines it cleared — a setting silently
36673
+ // shadowed is the one failure this whole command exists to fix.
36674
+ for (const l of layout.lines) l.theme = null;
36675
+ }
36676
+ const gl = flag("--glyphs");
36677
+ if (typeof gl === "string") {
36678
+ if (!STATUSLINE_GLYPHSETS[gl]) {
36679
+ if (wantsJson()) return emitJson({ ok: false, reason: "unknown-glyphs", glyphs: gl, known: Object.keys(STATUSLINE_GLYPHSETS) }, 2);
36680
+ console.error(`unknown symbol set "${gl}" — known: ${Object.keys(STATUSLINE_GLYPHSETS).join(", ")}`);
36681
+ process.exit(2);
36682
+ }
36683
+ layout.glyphs = gl;
36684
+ }
36685
+ const an = flag("--ansi");
36686
+ if (typeof an === "string") {
36687
+ if (!["auto", "off"].includes(an)) {
36688
+ if (wantsJson()) return emitJson({ ok: false, reason: "unknown-ansi", ansi: an, known: ["auto", "off"] }, 2);
36689
+ console.error(`--ansi takes auto or off`);
36690
+ process.exit(2);
36691
+ }
36692
+ layout.ansi = an;
36693
+ }
36694
+ const align = flag("--align-columns");
36695
+ if (typeof align === "string") layout.align_columns = align === "on";
36696
+ return slSaveAndCompile(claudeDir, layout, { action: "doc", theme: layout.theme, glyphs: layout.glyphs, ansi: layout.ansi, align_columns: !!layout.align_columns });
36697
+ }
36698
+
36502
36699
  function slPresetsCmd(claudeDir) {
36503
36700
  const layout = slReadLayout(claudeDir);
36504
36701
  const rows = slPresetsFor(slBoard().id).map(([name, p]) => {
package/bin/webui/api.js CHANGED
@@ -269,6 +269,11 @@ const READS = {
269
269
  const argv = ["statusline", "preview", ...slBoard(q)];
270
270
  if (q.width) argv.push("--width", String(q.width));
271
271
  if (q.state) argv.push("--state", String(q.state));
272
+ // v1.4.2 — a RENDER-ONLY override, so the panel can draw the same layout
273
+ // under each colour set and each symbol set. It writes nothing: the CLI
274
+ // applies these to a copy of the saved layout before it compiles.
275
+ if (q.theme) argv.push("--theme", String(q.theme));
276
+ if (q.glyphs) argv.push("--glyphs", String(q.glyphs));
272
277
  return argv;
273
278
  },
274
279
  "/api/statusline/explain": (q) => ["statusline", "explain", String(q.at || "1:1"), ...slBoard(q)],
@@ -483,6 +488,17 @@ const WRITES = {
483
488
  },
484
489
  // A preset REPLACES the layout, so the panel always confirms it and names
485
490
  // the loss — the `orc diy init --force` rule.
491
+ // THE DOCUMENT-LEVEL SETTINGS. `line` is per-LINE and `doc` is the whole
492
+ // layout; the colour set is a document fact, and routing it through `line`
493
+ // wrote one third of the bar while the picker read the other value back.
494
+ "/api/statusline/doc": (b) => {
495
+ const argv = ["statusline", "doc"];
496
+ if (b.theme) argv.push("--theme", String(b.theme));
497
+ if (b.glyphs) argv.push("--glyphs", String(b.glyphs));
498
+ if (b.ansi) argv.push("--ansi", String(b.ansi));
499
+ if (b.align_columns !== undefined) argv.push("--align-columns", b.align_columns ? "on" : "off");
500
+ return argv.concat(slBoard(b));
501
+ },
486
502
  "/api/statusline/apply": (b) => ["statusline", "apply", String(b.name), ...slBoard(b)],
487
503
  // v1.3.0 W5. `group` wraps 2-4 as one object, `expand` is its inverse and is
488
504
  // also how a composite becomes editable, `clone` is for two `config` chips on
@@ -37,6 +37,61 @@
37
37
  from { opacity: 0; transform: translateY(8px); }
38
38
  to { opacity: 1; transform: none; }
39
39
  }
40
+ /* ── the CLI Hook Interface (v1.4.1) ─────────────────────────────────────── */
41
+ /* The board is a thing you BUILD, so it moves the way a thing you build moves:
42
+ a chip arrives, a chip you just staged breathes once so you can find it, and
43
+ a row in the part picker slides in as the list narrows under your typing.
44
+ Every one of them is a FINITE animation with a real end state, so the cap in
45
+ the reduced-motion block below leaves each one at rest rather than frozen
46
+ part-way. */
47
+ .hk-chip { animation: hk-chip-in 200ms var(--ease) backwards; }
48
+ @keyframes hk-chip-in {
49
+ from { opacity: 0; transform: translateY(6px) scale(0.97); }
50
+ to { opacity: 1; transform: none; }
51
+ }
52
+ /* Staged, not written — and the one thing on the board that has to be findable
53
+ the instant it appears. Three pulses and it stops: an infinite one would have
54
+ to be REMOVED below rather than capped, and this one reads correctly at rest.
55
+ */
56
+ .hk-chip-new { animation: hk-chip-in 200ms var(--ease) backwards, hk-chip-mark 900ms var(--ease) 3; }
57
+ @keyframes hk-chip-mark {
58
+ 0%, 100% { box-shadow: inset 0 0 0 0 var(--accent); }
59
+ 50% { box-shadow: inset 0 0 0 2px var(--accent); }
60
+ }
61
+ /* Position, drop marker, and the zone's own border: transitions rather than
62
+ animations, so an interrupted drag never leaves a chip mid-flight. */
63
+ .hk-chip, .hk-zone, .hk-add, .hk-act, .hk-pick-row, .hk-theme, .hk-sample {
64
+ transition: border-color var(--dur) var(--ease), background var(--dur) var(--ease),
65
+ box-shadow var(--dur) var(--ease), opacity var(--dur) var(--ease),
66
+ transform var(--dur) var(--ease);
67
+ }
68
+ .hk-chip:hover:not(.hk-chip-dragging) { transform: translateY(-2px); }
69
+ .hk-add:hover:not(:disabled) { transform: translateY(-2px); }
70
+ .hk-pick-row { animation: hk-row-in 160ms var(--ease) backwards; }
71
+ @keyframes hk-row-in {
72
+ from { opacity: 0; transform: translateX(-6px); }
73
+ to { opacity: 1; transform: none; }
74
+ }
75
+ button.hk-pick-row:hover { transform: translateX(3px); }
76
+
77
+ /* NOTHING ARRIVES ON A REPAINT (v1.4.2). The CLI Hook Interface rebuilds its
78
+ whole panel when a change is STAGED — no request, no navigation, the same
79
+ cards with one value different — and the entrance animations above replayed
80
+ every time, on a 30ms stagger. With the network tab empty it read exactly
81
+ like the page reloading, on every keystroke.
82
+
83
+ `hk-quiet` is added by `hkPaint` from the SECOND paint onward and by the part
84
+ editor before every rebuild. It removes the ENTRANCES only: the transitions
85
+ further up are about the pointer rather than about arriving, so hover, the
86
+ drop marker and the staged edge are untouched. It must load after the rules
87
+ it switches off, which is why it is here and not in the panel's own file. */
88
+ .hk-quiet > *,
89
+ .hk-quiet .hk-chip,
90
+ .hk-quiet .hk-chip-new,
91
+ .hk-quiet .hk-pick-row {
92
+ animation: none;
93
+ }
94
+
40
95
  /* ---------------------------------------------------------- REDUCED MOTION */
41
96
  /* Non-negotiable: every animation and transition above is off. */
42
97
 
@@ -55,6 +110,12 @@
55
110
  nudges are removed outright rather than sped up. */
56
111
  .nav a:hover, .btn:active:not(:disabled), .action:hover,
57
112
  .todo:hover, .stat-link:hover, button.step:hover { transform: none !important; }
113
+ /* The board's own nudges, removed rather than shortened — a chip that lifts
114
+ under the pointer is an affordance, and a fast lift is still a lift. The
115
+ border and box-shadow changes stay: THEY are what says where a drop lands,
116
+ and a drop marker carried by motion alone would not exist here at all. */
117
+ .hk-chip:hover:not(.hk-chip-dragging), .hk-add:hover:not(:disabled),
118
+ button.hk-pick-row:hover { transform: none !important; }
58
119
  /* The vault pulse is one of two INFINITE animations on the page. Capping it to
59
120
  a single 1ms run would still leave a dash frozen mid-edge, so it is removed
60
121
  outright and the edge reads as a plain line. */
@@ -124,12 +124,14 @@
124
124
  way. The PREVIEW never collapses: it is a monospaced grid and squeezing it
125
125
  would misreport the width the user is deciding. It scrolls instead. */
126
126
  .hk-degrade-row,
127
+ .hk-compare-row,
127
128
  .hk-field,
128
129
  .hk-check {
129
130
  grid-template-columns: 1fr;
130
131
  gap: 4px;
131
132
  }
132
- .hk-degrade-label {
133
+ .hk-degrade-label,
134
+ .hk-compare-label {
133
135
  text-align: left;
134
136
  }
135
137
  .hk-gallery {
@@ -138,6 +140,20 @@
138
140
  .hk-prow-id {
139
141
  min-width: 0;
140
142
  }
143
+ /* v1.4.1 — the editor's two-column field (a name plus the sentence that
144
+ explains it, then the control) stacks; and DRAG IS OFF below this width. A
145
+ 40-pixel drop gap on a phone is not a target, so the three buttons on every
146
+ chip are the path — which is why they are buttons on the chip and not a
147
+ menu behind one. */
148
+ .hk-chip {
149
+ max-width: none;
150
+ flex-basis: 100%;
151
+ cursor: default;
152
+ }
153
+ .hk-grip { display: none; }
154
+ .hk-add { flex-basis: 100%; }
155
+ .hk-picker, .hk-editor { min-width: 0; }
156
+ .hk-theme { flex-basis: 100%; max-width: none; }
141
157
  /* The caution KEEPS its two columns. The mark and the sentence belong to each
142
158
  other, and stacking them puts a lone ✕ on a line of its own above the text
143
159
  it is about. The mark column narrows instead. */