@walwal-harness/cli 7.1.40 → 7.1.42

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.
Files changed (84) hide show
  1. package/HR-Resource/cdo/SKILL.md +9 -1
  2. package/HR-Resource/ceo/SKILL.md +33 -2
  3. package/HR-Resource/coo/SKILL.md +9 -1
  4. package/HR-Resource/cqo/SKILL.md +9 -1
  5. package/HR-Resource/cto/SKILL.md +10 -2
  6. package/HR-Resource/ops/SKILL.md +12 -4
  7. package/HR-Resource/ops-prompt-inspector/SKILL.md +11 -6
  8. package/HR-Resource/ops-prompt-inspector/assets/prompt-inspector-static.js +180 -0
  9. package/HR-Resource/ops-prompt-inspector/scripts/setup.py +66 -3
  10. package/apps/harness-dashboard/app/api/metrics/route.ts +17 -0
  11. package/apps/harness-dashboard/app/api/wake/route.ts +41 -0
  12. package/apps/harness-dashboard/app/brick.css +482 -0
  13. package/apps/harness-dashboard/app/layout.tsx +13 -0
  14. package/apps/harness-dashboard/app/page.tsx +2 -2
  15. package/apps/harness-dashboard/components/Scene.tsx +690 -36
  16. package/apps/harness-dashboard/components/brick/BrickDashboard.tsx +149 -0
  17. package/apps/harness-dashboard/components/brick/reports.tsx +291 -0
  18. package/apps/harness-dashboard/components/brick/tweaks.tsx +738 -0
  19. package/apps/harness-dashboard/components/brick/ui.tsx +463 -0
  20. package/apps/harness-dashboard/components/brick/views-graph.tsx +305 -0
  21. package/apps/harness-dashboard/components/brick/views-grid.tsx +543 -0
  22. package/apps/harness-dashboard/components/brick/views-timeline.tsx +238 -0
  23. package/apps/harness-dashboard/lib/__tests__/harness-state.test.ts +1 -202
  24. package/apps/harness-dashboard/lib/__tests__/sandbox-e2e.test.ts +490 -0
  25. package/apps/harness-dashboard/lib/brick/adapter.ts +423 -0
  26. package/apps/harness-dashboard/lib/brick/contract.ts +247 -0
  27. package/apps/harness-dashboard/lib/harness-state.ts +107 -796
  28. package/apps/harness-dashboard/lib/metrics/sampler.ts +545 -0
  29. package/apps/harness-dashboard/lib/types.ts +22 -222
  30. package/apps/harness-dashboard/playwright.config.ts +26 -7
  31. package/assets/templates/AGENTS-ko.md.template +1 -1
  32. package/assets/templates/AGENTS.md.template +12 -2
  33. package/assets/templates/HARNESS.md +2 -0
  34. package/assets/templates/company-pipeline-manifest.json +5 -14
  35. package/assets/templates/worker-report.md.template +38 -0
  36. package/bin/init.js +2 -1
  37. package/commands/goal.md +4 -2
  38. package/commands/hot-fix.md +1 -1
  39. package/commands/submission.md +1 -1
  40. package/package.json +1 -1
  41. package/scripts/conductor-tick.sh +12 -0
  42. package/scripts/harness-agenda.sh +105 -0
  43. package/scripts/harness-company-block.sh +89 -0
  44. package/scripts/harness-company-complete.sh +17 -8
  45. package/scripts/harness-company-cycle.sh +54 -0
  46. package/scripts/harness-progress-set.sh +8 -2
  47. package/scripts/harness-stop.sh +76 -0
  48. package/scripts/harness-wake-control.sh +151 -0
  49. package/scripts/harness-wake.sh +5 -0
  50. package/scripts/harness-worker-dispatch.sh +31 -7
  51. package/apps/harness-dashboard/components/Drawer.tsx +0 -146
  52. package/apps/harness-dashboard/components/Header.tsx +0 -40
  53. package/apps/harness-dashboard/components/MissionTimeline.tsx +0 -214
  54. package/apps/harness-dashboard/components/OrgTree.tsx +0 -459
  55. package/apps/harness-dashboard/components/drawer/AgentLogTab.tsx +0 -56
  56. package/apps/harness-dashboard/components/drawer/DesignPreviewTab.tsx +0 -160
  57. package/apps/harness-dashboard/components/drawer/GotchasTab.tsx +0 -89
  58. package/apps/harness-dashboard/components/drawer/HypothesisTab.tsx +0 -42
  59. package/apps/harness-dashboard/components/drawer/IncidentsTab.tsx +0 -50
  60. package/apps/harness-dashboard/components/drawer/MissionDocTab.tsx +0 -96
  61. package/apps/harness-dashboard/components/drawer/MissionFlowTab.tsx +0 -576
  62. package/apps/harness-dashboard/components/drawer/OwnerHistoryTab.tsx +0 -160
  63. package/apps/harness-dashboard/components/drawer/RoomMetricsTab.tsx +0 -90
  64. package/apps/harness-dashboard/components/drawer/TracksTab.tsx +0 -47
  65. package/apps/harness-dashboard/components/three/ArchiveBoxes.tsx +0 -95
  66. package/apps/harness-dashboard/components/three/DeptBoard.tsx +0 -175
  67. package/apps/harness-dashboard/components/three/Floor3D.tsx +0 -404
  68. package/apps/harness-dashboard/components/three/GoalCard3D.tsx +0 -123
  69. package/apps/harness-dashboard/components/three/HypothesisCards.tsx +0 -71
  70. package/apps/harness-dashboard/components/three/MeetingWhiteboard.tsx +0 -98
  71. package/apps/harness-dashboard/components/three/Minifig3D.tsx +0 -391
  72. package/apps/harness-dashboard/components/three/OpsSiren.tsx +0 -99
  73. package/apps/harness-dashboard/components/three/Stage3D.tsx +0 -104
  74. package/apps/harness-dashboard/components/three/TrackRibbons.tsx +0 -81
  75. package/apps/harness-dashboard/lib/__tests__/i18n.test.ts +0 -16
  76. package/apps/harness-dashboard/lib/__tests__/iso.test.ts +0 -42
  77. package/apps/harness-dashboard/lib/__tests__/state-mapping.test.ts +0 -183
  78. package/apps/harness-dashboard/lib/agent-roster.ts +0 -42
  79. package/apps/harness-dashboard/lib/furniture.ts +0 -165
  80. package/apps/harness-dashboard/lib/i18n.ts +0 -26
  81. package/apps/harness-dashboard/lib/iso.ts +0 -150
  82. package/apps/harness-dashboard/lib/markdown.tsx +0 -306
  83. package/apps/harness-dashboard/lib/path-planning.ts +0 -72
  84. package/apps/harness-dashboard/lib/state-mapping.ts +0 -146
@@ -0,0 +1,738 @@
1
+ "use client";
2
+
3
+ /* =============================================================
4
+ walwal-harness · BRICK OFFICE — Tweaks panel
5
+ Ported from the design-handoff mockup (harness/tweaks-panel.jsx).
6
+
7
+ Differences from the mockup (intentional, for the real Next.js app):
8
+ - No host edit-mode protocol (postMessage __edit_mode_*). That was a
9
+ design-tool artifact. Here the panel is self-contained: it opens by
10
+ default, the ✕ dismisses it, and a floating pill (.twk-fab) re-opens it.
11
+ - useTweaks persists to localStorage (SSR-safe: defaults on first render,
12
+ hydrate from storage in an effect to avoid hydration mismatch).
13
+ - All CSS lives in app/brick.css (the .twk-* block) — no inline <style>.
14
+
15
+ The four real tweaks drive the shell:
16
+ agentLayout card|list -> GridView layout
17
+ density compact|comfy -> [data-density] on the shell root
18
+ motion active|calm|off -> live-motion intensity passed to the adapter/views
19
+ surface midnight|slate -> [data-tone] on the shell root
20
+ ============================================================= */
21
+
22
+ import React, {
23
+ useCallback,
24
+ useEffect,
25
+ useMemo,
26
+ useRef,
27
+ useState,
28
+ } from "react";
29
+
30
+ /* ---- tweak value model ------------------------------------------------- */
31
+
32
+ export type AgentLayout = "card" | "list";
33
+ export type TweakDensity = "compact" | "comfy";
34
+ export type TweakMotion = "active" | "calm" | "off";
35
+ export type TweakSurface = "midnight" | "slate";
36
+
37
+ /** The persisted tweak state. */
38
+ export interface TweakValues {
39
+ agentLayout: AgentLayout;
40
+ density: TweakDensity;
41
+ motion: TweakMotion;
42
+ surface: TweakSurface;
43
+ }
44
+
45
+ export const TWEAK_DEFAULTS: TweakValues = {
46
+ agentLayout: "card",
47
+ density: "compact",
48
+ motion: "active",
49
+ surface: "slate",
50
+ };
51
+
52
+ const STORAGE_KEY = "brick.tweaks.v1";
53
+
54
+ /** Setter overloads: setTweak('key', value) or setTweak({ partial }). */
55
+ export type SetTweak = {
56
+ <K extends keyof TweakValues>(key: K, value: TweakValues[K]): void;
57
+ (edits: Partial<TweakValues>): void;
58
+ };
59
+
60
+ function readStored(): Partial<TweakValues> | null {
61
+ if (typeof window === "undefined") return null;
62
+ try {
63
+ const raw = window.localStorage.getItem(STORAGE_KEY);
64
+ if (!raw) return null;
65
+ const parsed: unknown = JSON.parse(raw);
66
+ if (parsed && typeof parsed === "object") {
67
+ return parsed as Partial<TweakValues>;
68
+ }
69
+ } catch {
70
+ // ignore corrupt/blocked storage
71
+ }
72
+ return null;
73
+ }
74
+
75
+ // ── useTweaks ───────────────────────────────────────────────────────────────
76
+ // Single source of truth for tweak values. setTweak persists to localStorage.
77
+ // SSR-safe: starts from defaults (so server and first client render match),
78
+ // then hydrates from storage in an effect.
79
+ export function useTweaks(
80
+ defaults: TweakValues = TWEAK_DEFAULTS
81
+ ): [TweakValues, SetTweak] {
82
+ const [values, setValues] = useState<TweakValues>(defaults);
83
+
84
+ useEffect(() => {
85
+ const stored = readStored();
86
+ if (stored) setValues((prev) => ({ ...prev, ...stored }));
87
+ }, []);
88
+
89
+ const setTweak = useCallback<SetTweak>(
90
+ (keyOrEdits: keyof TweakValues | Partial<TweakValues>, val?: unknown) => {
91
+ const edits: Partial<TweakValues> =
92
+ typeof keyOrEdits === "object" && keyOrEdits !== null
93
+ ? keyOrEdits
94
+ : ({ [keyOrEdits]: val } as Partial<TweakValues>);
95
+ setValues((prev) => {
96
+ const next = { ...prev, ...edits };
97
+ try {
98
+ window.localStorage.setItem(STORAGE_KEY, JSON.stringify(next));
99
+ } catch {
100
+ // ignore blocked storage
101
+ }
102
+ return next;
103
+ });
104
+ },
105
+ []
106
+ );
107
+
108
+ return [values, setTweak];
109
+ }
110
+
111
+ // ── TweaksPanel ─────────────────────────────────────────────────────────────
112
+ // Floating, draggable shell. Open by default; ✕ dismisses to a floating pill
113
+ // that re-opens it. Clamps itself to the viewport on resize.
114
+
115
+ const PAD = 16;
116
+
117
+ interface TweaksPanelProps {
118
+ title?: string;
119
+ children?: React.ReactNode;
120
+ }
121
+
122
+ export function TweaksPanel({
123
+ title = "Tweaks",
124
+ children,
125
+ }: TweaksPanelProps): React.JSX.Element {
126
+ const [open, setOpen] = useState(true);
127
+ const dragRef = useRef<HTMLDivElement | null>(null);
128
+ const offsetRef = useRef<{ x: number; y: number }>({ x: PAD, y: PAD });
129
+
130
+ const clampToViewport = useCallback(() => {
131
+ const panel = dragRef.current;
132
+ if (!panel) return;
133
+ const w = panel.offsetWidth;
134
+ const h = panel.offsetHeight;
135
+ const maxRight = Math.max(PAD, window.innerWidth - w - PAD);
136
+ const maxBottom = Math.max(PAD, window.innerHeight - h - PAD);
137
+ offsetRef.current = {
138
+ x: Math.min(maxRight, Math.max(PAD, offsetRef.current.x)),
139
+ y: Math.min(maxBottom, Math.max(PAD, offsetRef.current.y)),
140
+ };
141
+ panel.style.right = `${offsetRef.current.x}px`;
142
+ panel.style.bottom = `${offsetRef.current.y}px`;
143
+ }, []);
144
+
145
+ useEffect(() => {
146
+ if (!open) return;
147
+ clampToViewport();
148
+ if (typeof ResizeObserver === "undefined") {
149
+ window.addEventListener("resize", clampToViewport);
150
+ return () => window.removeEventListener("resize", clampToViewport);
151
+ }
152
+ const ro = new ResizeObserver(clampToViewport);
153
+ ro.observe(document.documentElement);
154
+ return () => ro.disconnect();
155
+ }, [open, clampToViewport]);
156
+
157
+ const onDragStart = (e: React.MouseEvent) => {
158
+ const panel = dragRef.current;
159
+ if (!panel) return;
160
+ const r = panel.getBoundingClientRect();
161
+ const sx = e.clientX;
162
+ const sy = e.clientY;
163
+ const startRight = window.innerWidth - r.right;
164
+ const startBottom = window.innerHeight - r.bottom;
165
+ const move = (ev: MouseEvent) => {
166
+ offsetRef.current = {
167
+ x: startRight - (ev.clientX - sx),
168
+ y: startBottom - (ev.clientY - sy),
169
+ };
170
+ clampToViewport();
171
+ };
172
+ const up = () => {
173
+ window.removeEventListener("mousemove", move);
174
+ window.removeEventListener("mouseup", up);
175
+ };
176
+ window.addEventListener("mousemove", move);
177
+ window.addEventListener("mouseup", up);
178
+ };
179
+
180
+ if (!open) {
181
+ return (
182
+ <button
183
+ type="button"
184
+ className="twk-fab"
185
+ aria-label="Open tweaks"
186
+ onClick={() => setOpen(true)}
187
+ >
188
+ {title}
189
+ </button>
190
+ );
191
+ }
192
+
193
+ return (
194
+ <div
195
+ ref={dragRef}
196
+ className="twk-panel"
197
+ style={{ right: offsetRef.current.x, bottom: offsetRef.current.y }}
198
+ >
199
+ <div className="twk-hd" onMouseDown={onDragStart}>
200
+ <b>{title}</b>
201
+ <button
202
+ className="twk-x"
203
+ type="button"
204
+ aria-label="Close tweaks"
205
+ onMouseDown={(e) => e.stopPropagation()}
206
+ onClick={() => setOpen(false)}
207
+ >
208
+ ✕
209
+ </button>
210
+ </div>
211
+ <div className="twk-body">{children}</div>
212
+ </div>
213
+ );
214
+ }
215
+
216
+ // ── Layout helpers ──────────────────────────────────────────────────────────
217
+
218
+ interface TweakSectionProps {
219
+ label: string;
220
+ children?: React.ReactNode;
221
+ }
222
+
223
+ export function TweakSection({
224
+ label,
225
+ children,
226
+ }: TweakSectionProps): React.JSX.Element {
227
+ return (
228
+ <>
229
+ <div className="twk-sect">{label}</div>
230
+ {children}
231
+ </>
232
+ );
233
+ }
234
+
235
+ interface TweakRowProps {
236
+ label: string;
237
+ value?: React.ReactNode;
238
+ children?: React.ReactNode;
239
+ inline?: boolean;
240
+ }
241
+
242
+ export function TweakRow({
243
+ label,
244
+ value,
245
+ children,
246
+ inline = false,
247
+ }: TweakRowProps): React.JSX.Element {
248
+ return (
249
+ <div className={inline ? "twk-row twk-row-h" : "twk-row"}>
250
+ <div className="twk-lbl">
251
+ <span>{label}</span>
252
+ {value != null && <span className="twk-val">{value}</span>}
253
+ </div>
254
+ {children}
255
+ </div>
256
+ );
257
+ }
258
+
259
+ // ── Controls ────────────────────────────────────────────────────────────────
260
+
261
+ /** An option for the segmented / select / color controls. */
262
+ export type TweakOption<T> = T | { value: T; label: string };
263
+
264
+ interface TweakSliderProps {
265
+ label: string;
266
+ value: number;
267
+ min?: number;
268
+ max?: number;
269
+ step?: number;
270
+ unit?: string;
271
+ onChange: (v: number) => void;
272
+ }
273
+
274
+ export function TweakSlider({
275
+ label,
276
+ value,
277
+ min = 0,
278
+ max = 100,
279
+ step = 1,
280
+ unit = "",
281
+ onChange,
282
+ }: TweakSliderProps): React.JSX.Element {
283
+ return (
284
+ <TweakRow label={label} value={`${value}${unit}`}>
285
+ <input
286
+ type="range"
287
+ className="twk-slider"
288
+ min={min}
289
+ max={max}
290
+ step={step}
291
+ value={value}
292
+ onChange={(e) => onChange(Number(e.target.value))}
293
+ />
294
+ </TweakRow>
295
+ );
296
+ }
297
+
298
+ interface TweakToggleProps {
299
+ label: string;
300
+ value: boolean;
301
+ onChange: (v: boolean) => void;
302
+ }
303
+
304
+ export function TweakToggle({
305
+ label,
306
+ value,
307
+ onChange,
308
+ }: TweakToggleProps): React.JSX.Element {
309
+ return (
310
+ <div className="twk-row twk-row-h">
311
+ <div className="twk-lbl">
312
+ <span>{label}</span>
313
+ </div>
314
+ <button
315
+ type="button"
316
+ className="twk-toggle"
317
+ data-on={value ? "1" : "0"}
318
+ role="switch"
319
+ aria-checked={value}
320
+ onClick={() => onChange(!value)}
321
+ >
322
+ <i />
323
+ </button>
324
+ </div>
325
+ );
326
+ }
327
+
328
+ function optLabel<T>(o: TweakOption<T>): string {
329
+ return typeof o === "object" && o !== null && "label" in o
330
+ ? (o as { label: string }).label
331
+ : String(o);
332
+ }
333
+ function optValue<T>(o: TweakOption<T>): T {
334
+ return typeof o === "object" && o !== null && "value" in o
335
+ ? (o as { value: T }).value
336
+ : (o as T);
337
+ }
338
+
339
+ interface TweakRadioProps<T> {
340
+ label: string;
341
+ value: T;
342
+ options: ReadonlyArray<TweakOption<T>>;
343
+ onChange: (v: T) => void;
344
+ }
345
+
346
+ export function TweakRadio<T>({
347
+ label,
348
+ value,
349
+ options,
350
+ onChange,
351
+ }: TweakRadioProps<T>): React.JSX.Element {
352
+ const trackRef = useRef<HTMLDivElement | null>(null);
353
+ const [dragging, setDragging] = useState(false);
354
+ // The active value is read by pointer-move handlers attached for the lifetime
355
+ // of a drag — ref it so a stale closure doesn't fire onChange for every move.
356
+ const valueRef = useRef<T>(value);
357
+ valueRef.current = value;
358
+
359
+ // Segments wrap mid-word once per-segment width runs out (~16 chars for 2
360
+ // options, ~10 for 3). Past that (or >3 options) fall back to a dropdown.
361
+ const maxLen = options.reduce((m, o) => Math.max(m, optLabel(o).length), 0);
362
+ const fitMap: Record<number, number> = { 2: 16, 3: 10 };
363
+ const fitsAsSegments = maxLen <= (fitMap[options.length] ?? 0);
364
+
365
+ const opts = useMemo(
366
+ () =>
367
+ options.map((o) => ({ value: optValue(o), label: optLabel(o) })),
368
+ [options]
369
+ );
370
+ const idx = Math.max(0, opts.findIndex((o) => o.value === value));
371
+ const n = opts.length;
372
+
373
+ const segAt = useCallback(
374
+ (clientX: number): T => {
375
+ const track = trackRef.current;
376
+ if (!track) return valueRef.current;
377
+ const r = track.getBoundingClientRect();
378
+ const inner = r.width - 4;
379
+ const i = Math.floor(((clientX - r.left - 2) / inner) * n);
380
+ return opts[Math.max(0, Math.min(n - 1, i))].value;
381
+ },
382
+ [n, opts]
383
+ );
384
+
385
+ if (!fitsAsSegments) {
386
+ return (
387
+ <TweakSelect
388
+ label={label}
389
+ value={value}
390
+ options={options}
391
+ onChange={onChange}
392
+ />
393
+ );
394
+ }
395
+
396
+ const onPointerDown = (e: React.PointerEvent) => {
397
+ setDragging(true);
398
+ const v0 = segAt(e.clientX);
399
+ if (v0 !== valueRef.current) onChange(v0);
400
+ const move = (ev: PointerEvent) => {
401
+ if (!trackRef.current) return;
402
+ const v = segAt(ev.clientX);
403
+ if (v !== valueRef.current) onChange(v);
404
+ };
405
+ const up = () => {
406
+ setDragging(false);
407
+ window.removeEventListener("pointermove", move);
408
+ window.removeEventListener("pointerup", up);
409
+ };
410
+ window.addEventListener("pointermove", move);
411
+ window.addEventListener("pointerup", up);
412
+ };
413
+
414
+ return (
415
+ <TweakRow label={label}>
416
+ <div
417
+ ref={trackRef}
418
+ role="radiogroup"
419
+ onPointerDown={onPointerDown}
420
+ className={dragging ? "twk-seg dragging" : "twk-seg"}
421
+ >
422
+ <div
423
+ className="twk-seg-thumb"
424
+ style={{
425
+ left: `calc(2px + ${idx} * (100% - 4px) / ${n})`,
426
+ width: `calc((100% - 4px) / ${n})`,
427
+ }}
428
+ />
429
+ {opts.map((o) => (
430
+ <button
431
+ key={String(o.value)}
432
+ type="button"
433
+ role="radio"
434
+ aria-checked={o.value === value}
435
+ onClick={() => onChange(o.value)}
436
+ >
437
+ {o.label}
438
+ </button>
439
+ ))}
440
+ </div>
441
+ </TweakRow>
442
+ );
443
+ }
444
+
445
+ interface TweakSelectProps<T> {
446
+ label: string;
447
+ value: T;
448
+ options: ReadonlyArray<TweakOption<T>>;
449
+ onChange: (v: T) => void;
450
+ }
451
+
452
+ export function TweakSelect<T>({
453
+ label,
454
+ value,
455
+ options,
456
+ onChange,
457
+ }: TweakSelectProps<T>): React.JSX.Element {
458
+ // <select> emits strings; map the chosen string back to the original
459
+ // option value so the control stays type-preserving.
460
+ const resolve = (s: string): T => {
461
+ const m = options.find((o) => String(optValue(o)) === s);
462
+ return m === undefined ? (s as unknown as T) : optValue(m);
463
+ };
464
+ return (
465
+ <TweakRow label={label}>
466
+ <select
467
+ className="twk-field"
468
+ value={String(value)}
469
+ onChange={(e) => onChange(resolve(e.target.value))}
470
+ >
471
+ {options.map((o) => {
472
+ const v = optValue(o);
473
+ return (
474
+ <option key={String(v)} value={String(v)}>
475
+ {optLabel(o)}
476
+ </option>
477
+ );
478
+ })}
479
+ </select>
480
+ </TweakRow>
481
+ );
482
+ }
483
+
484
+ interface TweakTextProps {
485
+ label: string;
486
+ value: string;
487
+ placeholder?: string;
488
+ onChange: (v: string) => void;
489
+ }
490
+
491
+ export function TweakText({
492
+ label,
493
+ value,
494
+ placeholder,
495
+ onChange,
496
+ }: TweakTextProps): React.JSX.Element {
497
+ return (
498
+ <TweakRow label={label}>
499
+ <input
500
+ className="twk-field"
501
+ type="text"
502
+ value={value}
503
+ placeholder={placeholder}
504
+ onChange={(e) => onChange(e.target.value)}
505
+ />
506
+ </TweakRow>
507
+ );
508
+ }
509
+
510
+ interface TweakNumberProps {
511
+ label: string;
512
+ value: number;
513
+ min?: number;
514
+ max?: number;
515
+ step?: number;
516
+ unit?: string;
517
+ onChange: (v: number) => void;
518
+ }
519
+
520
+ export function TweakNumber({
521
+ label,
522
+ value,
523
+ min,
524
+ max,
525
+ step = 1,
526
+ unit = "",
527
+ onChange,
528
+ }: TweakNumberProps): React.JSX.Element {
529
+ const clamp = (n: number): number => {
530
+ if (min != null && n < min) return min;
531
+ if (max != null && n > max) return max;
532
+ return n;
533
+ };
534
+ const startRef = useRef<{ x: number; val: number }>({ x: 0, val: 0 });
535
+ const onScrubStart = (e: React.PointerEvent) => {
536
+ e.preventDefault();
537
+ startRef.current = { x: e.clientX, val: value };
538
+ const decimals = (String(step).split(".")[1] ?? "").length;
539
+ const move = (ev: PointerEvent) => {
540
+ const dx = ev.clientX - startRef.current.x;
541
+ const raw = startRef.current.val + dx * step;
542
+ const snapped = Math.round(raw / step) * step;
543
+ onChange(clamp(Number(snapped.toFixed(decimals))));
544
+ };
545
+ const up = () => {
546
+ window.removeEventListener("pointermove", move);
547
+ window.removeEventListener("pointerup", up);
548
+ };
549
+ window.addEventListener("pointermove", move);
550
+ window.addEventListener("pointerup", up);
551
+ };
552
+ return (
553
+ <div className="twk-num">
554
+ <span className="twk-num-lbl" onPointerDown={onScrubStart}>
555
+ {label}
556
+ </span>
557
+ <input
558
+ type="number"
559
+ value={value}
560
+ min={min}
561
+ max={max}
562
+ step={step}
563
+ onChange={(e) => onChange(clamp(Number(e.target.value)))}
564
+ />
565
+ {unit && <span className="twk-num-unit">{unit}</span>}
566
+ </div>
567
+ );
568
+ }
569
+
570
+ // Relative-luminance contrast pick — a checkmark over a swatch must read on
571
+ // both dark and light fills. Hex input only (#rgb / #rrggbb).
572
+ function twkIsLight(hex: string): boolean {
573
+ const h = String(hex).replace("#", "");
574
+ const x = h.length === 3 ? h.replace(/./g, (c) => c + c) : h.padEnd(6, "0");
575
+ const n = parseInt(x.slice(0, 6), 16);
576
+ if (Number.isNaN(n)) return true;
577
+ const r = (n >> 16) & 255;
578
+ const g = (n >> 8) & 255;
579
+ const b = n & 255;
580
+ return r * 299 + g * 587 + b * 114 > 148000;
581
+ }
582
+
583
+ function TwkCheck({ light }: { light: boolean }): React.JSX.Element {
584
+ return (
585
+ <svg viewBox="0 0 14 14" aria-hidden="true">
586
+ <path
587
+ d="M3 7.2 5.8 10 11 4.2"
588
+ fill="none"
589
+ strokeWidth="2.2"
590
+ strokeLinecap="round"
591
+ strokeLinejoin="round"
592
+ stroke={light ? "rgba(0,0,0,.78)" : "#fff"}
593
+ />
594
+ </svg>
595
+ );
596
+ }
597
+
598
+ /** A color tweak option: a single hex string or a 1-5 hex palette array. */
599
+ export type ColorOption = string | string[];
600
+
601
+ interface TweakColorProps {
602
+ label: string;
603
+ value: ColorOption;
604
+ options?: ReadonlyArray<ColorOption>;
605
+ onChange: (v: ColorOption) => void;
606
+ }
607
+
608
+ export function TweakColor({
609
+ label,
610
+ value,
611
+ options,
612
+ onChange,
613
+ }: TweakColorProps): React.JSX.Element {
614
+ if (!options || !options.length) {
615
+ return (
616
+ <div className="twk-row twk-row-h">
617
+ <div className="twk-lbl">
618
+ <span>{label}</span>
619
+ </div>
620
+ <input
621
+ type="color"
622
+ className="twk-swatch"
623
+ value={typeof value === "string" ? value : (value[0] ?? "#000000")}
624
+ onChange={(e) => onChange(e.target.value)}
625
+ />
626
+ </div>
627
+ );
628
+ }
629
+ // Native <input type=color> emits lowercase hex, so compare lowercased.
630
+ const key = (o: ColorOption): string =>
631
+ String(JSON.stringify(o)).toLowerCase();
632
+ const cur = key(value);
633
+ return (
634
+ <TweakRow label={label}>
635
+ <div className="twk-chips" role="radiogroup">
636
+ {options.map((o, i) => {
637
+ const colors = Array.isArray(o) ? o : [o];
638
+ const hero = colors[0] ?? "#000000";
639
+ const sup = colors.slice(1, 5);
640
+ const on = key(o) === cur;
641
+ return (
642
+ <button
643
+ key={i}
644
+ type="button"
645
+ className="twk-chip"
646
+ role="radio"
647
+ aria-checked={on}
648
+ data-on={on ? "1" : "0"}
649
+ aria-label={colors.join(", ")}
650
+ title={colors.join(" · ")}
651
+ style={{ background: hero }}
652
+ onClick={() => onChange(o)}
653
+ >
654
+ {sup.length > 0 && (
655
+ <span>
656
+ {sup.map((c, j) => (
657
+ <i key={j} style={{ background: c }} />
658
+ ))}
659
+ </span>
660
+ )}
661
+ {on && <TwkCheck light={twkIsLight(hero)} />}
662
+ </button>
663
+ );
664
+ })}
665
+ </div>
666
+ </TweakRow>
667
+ );
668
+ }
669
+
670
+ interface TweakButtonProps {
671
+ label: string;
672
+ onClick: () => void;
673
+ secondary?: boolean;
674
+ }
675
+
676
+ export function TweakButton({
677
+ label,
678
+ onClick,
679
+ secondary = false,
680
+ }: TweakButtonProps): React.JSX.Element {
681
+ return (
682
+ <button
683
+ type="button"
684
+ className={secondary ? "twk-btn secondary" : "twk-btn"}
685
+ onClick={onClick}
686
+ >
687
+ {label}
688
+ </button>
689
+ );
690
+ }
691
+
692
+ // ── BrickTweaks ───────────────────────────────────────────────────────────
693
+ // The wired-up panel for the Brick Office dashboard: the four real tweaks
694
+ // (agent layout, density, motion, surface). Mirrors the mockup App() block.
695
+
696
+ interface BrickTweaksProps {
697
+ values: TweakValues;
698
+ setTweak: SetTweak;
699
+ title?: string;
700
+ }
701
+
702
+ export function BrickTweaks({
703
+ values,
704
+ setTweak,
705
+ title = "Tweaks",
706
+ }: BrickTweaksProps): React.JSX.Element {
707
+ return (
708
+ <TweaksPanel title={title}>
709
+ <TweakSection label="View" />
710
+ <TweakRadio<AgentLayout>
711
+ label="Agent layout"
712
+ value={values.agentLayout}
713
+ options={["card", "list"]}
714
+ onChange={(v) => setTweak("agentLayout", v)}
715
+ />
716
+ <TweakRadio<TweakDensity>
717
+ label="Density"
718
+ value={values.density}
719
+ options={["compact", "comfy"]}
720
+ onChange={(v) => setTweak("density", v)}
721
+ />
722
+ <TweakSection label="Motion" />
723
+ <TweakRadio<TweakMotion>
724
+ label="Live motion"
725
+ value={values.motion}
726
+ options={["active", "calm", "off"]}
727
+ onChange={(v) => setTweak("motion", v)}
728
+ />
729
+ <TweakSection label="Surface" />
730
+ <TweakRadio<TweakSurface>
731
+ label="Background"
732
+ value={values.surface}
733
+ options={["midnight", "slate"]}
734
+ onChange={(v) => setTweak("surface", v)}
735
+ />
736
+ </TweaksPanel>
737
+ );
738
+ }