makersclaw 0.3.2 → 0.5.1

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 (121) hide show
  1. package/README.md +3 -1
  2. package/package.json +1 -1
  3. package/r/annotate.json +3 -3
  4. package/r/approval-request.json +4 -4
  5. package/r/audio-player.json +2 -2
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +2 -2
  8. package/r/calendar-view.json +9 -9
  9. package/r/cart.json +2 -2
  10. package/r/chart.json +7 -7
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +2 -2
  13. package/r/comment-box.json +5 -5
  14. package/r/contact-section.json +2 -2
  15. package/r/core.json +17 -11
  16. package/r/csv-import.json +3 -3
  17. package/r/data-table.json +4 -4
  18. package/r/diagram.json +2 -2
  19. package/r/doc-editor.json +2 -2
  20. package/r/doc-page.json +3 -3
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +2 -2
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +2 -2
  26. package/r/gantt.json +3 -3
  27. package/r/job-panel.json +8 -5
  28. package/r/json-editor.json +3 -3
  29. package/r/map.json +2 -2
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +6 -6
  32. package/r/navigation-menu.json +2 -2
  33. package/r/order-detail.json +2 -2
  34. package/r/org-chart.json +4 -4
  35. package/r/page-builder.json +2 -2
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +3 -3
  39. package/r/product-detail.json +2 -2
  40. package/r/product-grid.json +3 -3
  41. package/r/record-form.json +2 -2
  42. package/r/record-notes.json +6 -6
  43. package/r/registry.json +131 -123
  44. package/r/search-dialog.json +2 -2
  45. package/r/settings-list.json +2 -2
  46. package/r/setup-checklist.json +2 -2
  47. package/r/share-dialog.json +2 -2
  48. package/r/sidebar.json +4 -4
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +2 -2
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +3 -3
  53. package/r/task-list.json +3 -3
  54. package/r/task-model.json +2 -2
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +2 -2
  57. package/r/whiteboard.json +3 -3
  58. package/registry/blocks/approval-request/README.md +2 -1
  59. package/registry/blocks/approval-request/approval-request.jsx +5 -3
  60. package/registry/blocks/calendar-view/README.md +8 -0
  61. package/registry/blocks/calendar-view/agenda.jsx +5 -4
  62. package/registry/blocks/calendar-view/calendar.jsx +23 -8
  63. package/registry/blocks/calendar-view/demo.jsx +2 -0
  64. package/registry/blocks/calendar-view/month.jsx +6 -5
  65. package/registry/blocks/calendar-view/time-grid.jsx +9 -7
  66. package/registry/blocks/calendar-view/tray.jsx +5 -4
  67. package/registry/blocks/chart/README.md +9 -4
  68. package/registry/blocks/chart/cartesian.jsx +31 -24
  69. package/registry/blocks/chart/core.jsx +21 -0
  70. package/registry/blocks/chart/pie.jsx +15 -10
  71. package/registry/blocks/chart/sparkline.jsx +32 -14
  72. package/registry/blocks/chat/message.jsx +4 -4
  73. package/registry/blocks/comment-box/README.md +1 -1
  74. package/registry/blocks/comment-box/comment-time.jsx +4 -6
  75. package/registry/blocks/data-table/README.md +4 -0
  76. package/registry/blocks/data-table/data-table.jsx +20 -6
  77. package/registry/blocks/flow-canvas/edges.jsx +4 -2
  78. package/registry/blocks/flow-canvas/theme.js +59 -48
  79. package/registry/blocks/job-panel/README.md +40 -3
  80. package/registry/blocks/job-panel/block.json +2 -2
  81. package/registry/blocks/job-panel/demo.jsx +75 -6
  82. package/registry/blocks/job-panel/job-panel.jsx +60 -11
  83. package/registry/blocks/mind-map/README.md +9 -2
  84. package/registry/blocks/mind-map/mind-map.jsx +51 -12
  85. package/registry/blocks/mind-map/tree.js +33 -0
  86. package/registry/blocks/org-chart/org-chart.jsx +1 -1
  87. package/registry/blocks/pdf-viewer/notes.jsx +4 -3
  88. package/registry/blocks/record-notes/README.md +1 -1
  89. package/registry/blocks/record-notes/record-notes.jsx +4 -5
  90. package/registry/blocks/sidebar/sidebar-panel.jsx +4 -4
  91. package/registry/blocks/sidebar/sidebar.jsx +13 -13
  92. package/registry/blocks/signature-pad/signature.js +8 -1
  93. package/registry/blocks/spreadsheet/cells.jsx +2 -2
  94. package/src/shadcn.mjs +1 -1
  95. package/templates/next/files/README.md +410 -86
  96. package/templates/next/files/app/auto-refresh.jsx +42 -15
  97. package/templates/next/files/app/claw-alive/route.js +17 -0
  98. package/templates/next/files/app/error-relay.jsx +9 -4
  99. package/templates/next/files/app/escape-relay.jsx +27 -0
  100. package/templates/next/files/app/frame.js +409 -0
  101. package/templates/next/files/app/layout.jsx +18 -3
  102. package/templates/next/files/check/run.mjs +51 -18
  103. package/templates/next/files/components/parts/fixture.jsx +29 -4
  104. package/templates/next/files/components/parts/index.js +2 -1
  105. package/templates/next/files/components/parts/live-work.jsx +235 -0
  106. package/templates/next/files/components/ui/button.jsx +6 -1
  107. package/templates/next/files/components/ui/calendar.jsx +24 -0
  108. package/templates/next/files/components/ui/date-picker.jsx +15 -2
  109. package/templates/next/files/components/ui/field.jsx +12 -3
  110. package/templates/next/files/components/ui/icon.jsx +37 -2
  111. package/templates/next/files/components/ui/index.js +1 -0
  112. package/templates/next/files/components/ui/sheet.jsx +38 -2
  113. package/templates/next/files/components/ui/toggle.jsx +20 -2
  114. package/templates/next/files/lib/actions.js +63 -5
  115. package/templates/next/files/lib/claw.js +127 -28
  116. package/templates/next/files/lib/data.js +4 -1
  117. package/templates/next/files/lib/db.js +18 -1
  118. package/templates/next/files/lib/hooks.js +493 -37
  119. package/templates/next/files/lib/platform.js +113 -15
  120. package/templates/next/files/surfaces/README.md +3 -0
  121. package/templates/next/template.json +19 -8
@@ -48,7 +48,7 @@ const STATIC_ONLY = process.argv.includes("--static");
48
48
  // update` lays both folders again.
49
49
  // Exact names AND extensions: Next resolves .js before .jsx, so a
50
50
  // components/ui/depiction.js beside depiction.jsx would win unseen.
51
- const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher)\.jsx)$/;
51
+ const KIT = /^components\/(?:ui\/(?:cx|index)\.js|ui\/(?:radio|toggle|button-group|avatar|breadcrumb|pagination|button|collapsible|item|table|depiction|feedback|stat|distribution|swatch|steps|separator|scroll-area|field|icon|menu|context-menu|popover|calendar|date-picker|tooltip|notice|sheet|tabs|slider|input-group|image|file-input|input-otp|command|combobox|rating|toast|carousel|resizable)\.jsx|parts\/(?:index|sample)\.js|parts\/(?:fixture|record-switcher|live-work)\.jsx)$/;
52
52
 
53
53
  // The pins: sha256 of every fixed file, written by `makersclaw init` and
54
54
  // `makersclaw add` (check/pins.json). Read first, because a BLOCK is
@@ -73,17 +73,35 @@ const DEPICTION = /^components\/depictions\//;
73
73
  * preview can follow typing). Never the shell, the kit or lib/. */
74
74
  const FRAME_HOST = /^(?:surfaces\/[^/]+\.jsx|components\/[^/]+\.jsx)$/;
75
75
 
76
+ // Folders a build, an install or a tool leaves in the app, never its
77
+ // source: ONE list for every walk here — the rules, the smoke build's
78
+ // freshness, its copy in the sandbox. Until 0.5.0 each walk named its own
79
+ // two or three, and the rules read a temporary .next-stub a build had
80
+ // left (phase-planner, 2026-09-23; CLAW-FIXES #36). A name ending in *
81
+ // is a prefix (.next, .next-check, .next-stub…). node_modules and .git
82
+ // are skipped wherever they are; the rest at the app's root only, where
83
+ // the build puts them: a folder of the app's own may be called out/.
84
+ const BUILD_DIRS = ["node_modules", ".git", ".next*", "out", ".turbo", ".cache"];
85
+ const ANY_DEPTH = new Set(["node_modules", ".git"]);
86
+ const isBuildDir = (name) => BUILD_DIRS.some((d) => (d.endsWith("*") ? name.startsWith(d.slice(0, -1)) : name === d));
87
+ /** Is this path, relative to the app, inside a folder that is not source? */
88
+ function notSource(rel) {
89
+ const parts = rel.split("/");
90
+ return isBuildDir(parts[0]) || parts.some((part) => ANY_DEPTH.has(part));
91
+ }
92
+
76
93
  // Files the rules skip: the kit (it owns the native elements and the
77
- // fallback colours), the fixed template files, machine folders. Depiction
78
- // files are NOT skipped — the `depictions` rule reads them.
94
+ // fallback colours), the fixed template files, the check itself. Build
95
+ // folders never reach this list (walk, below). Depiction files are NOT
96
+ // skipped — the `depictions` rule reads them.
79
97
  const SKIP = [
80
- /^node_modules\//,
81
- /^\.next(-check)?\//,
82
98
  /^check\//,
83
99
  KIT,
84
100
  /^app\/globals\.css$/,
85
101
  /^lib\/(db|data|actions|claw|platform|hooks|sample)\.js$/,
86
- /^app\/(layout|page|error|global-error|not-found|error-relay|auto-refresh)\.jsx$/,
102
+ /^app\/(layout|page|error|global-error|not-found|error-relay|auto-refresh|escape-relay)\.jsx$/,
103
+ /^app\/frame\.js$/,
104
+ /^app\/claw-alive\/route\.js$/,
87
105
  /^app\/s\/\[key\]\/page\.jsx$/,
88
106
  /^middleware\.js$/,
89
107
  /^next\.config\.mjs$/,
@@ -92,12 +110,12 @@ const SKIP = [
92
110
  /\.(md|sql|json|lock)$/,
93
111
  ];
94
112
 
95
- // Only the build's own folders are skipped: a dot-named module
113
+ // Only build folders are skipped (BUILD_DIRS): a dot-named module
96
114
  // (components/.pictures.js) is a module Next bundles all the same.
97
115
  function walk(dir, out = []) {
98
116
  for (const name of readdirSync(dir)) {
99
- if (name === "node_modules" || name === ".git" || name === ".next" || name === ".next-check") continue;
100
117
  const full = join(dir, name);
118
+ if (notSource(relative(ROOT, full))) continue;
101
119
  if (statSync(full).isDirectory()) walk(full, out);
102
120
  else out.push(full);
103
121
  }
@@ -223,7 +241,10 @@ const ATTRS = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'{])*`
223
241
  if (!L.count) ok("controls", "every control is a kit part");
224
242
  }
225
243
 
226
- // 3. The example is a fixture in code — never a row in the shared database.
244
+ // 3. The example is a fixture in code — never a row in the shared database
245
+ // — and since 0.4.0 it shows only outside a workspace: useFixture reads
246
+ // the layout's <Examples allowed>, off in every workspace app, where an
247
+ // empty table shows its live work (LiveEmpty) instead.
227
248
  {
228
249
  const L = limiter("examples");
229
250
  scan(/\bINSERT\s+INTO\b/i, (f, m, line) => L.hit(`${f.path}:${line}`, "insert", "SQL INSERT outside lib/actions.js; insertRow() is the write door, and a fixture is never inserted"), files);
@@ -234,11 +255,14 @@ const ATTRS = String.raw`(?:"[^"]*"|'[^']*'|\{(?:[^{}]|\{[^{}]*\})*\}|[^>"'{])*`
234
255
  // them is the one that must run them through useFixture(). A depiction
235
256
  // never holds one: its rows arrive as props from the surface.
236
257
  const renders = /<\/|\/>/.test(f.code);
237
- const uses = /\buseFixture\(|\bmarkSample\(/.test(f.code);
258
+ // Only useFixture reads the layout's <Examples allowed>: markSample
259
+ // gives rows their ids and nothing else, so a surface that marks its
260
+ // own fixture would show it inside a workspace too.
261
+ const uses = /\buseFixture\(/.test(f.code);
238
262
  const server = /from\s*["']\.\.?\/lib\/data["']/.test(f.code);
239
- if (renders && !uses) L.hit(`${f.path}:${lineOf(f.code, decl.index)}`, "fixture", `${decl[1]} is rendered without useFixture() — the example shows only while the table is empty, and is never saved${DEPICTION.test(f.path) ? " (a depiction takes its rows as props; the surface holds the fixture)" : server ? " (a server surface cannot call it: pass the rows down and let the client component run useFixture)" : ""}`);
263
+ if (renders && !uses) L.hit(`${f.path}:${lineOf(f.code, decl.index)}`, "fixture", `${decl[1]} is rendered without useFixture() — examples show only outside a workspace (useFixture); a workspace app opens empty${DEPICTION.test(f.path) ? " (a depiction takes its rows as props; the surface holds the fixture)" : server ? " (a server surface cannot call it: pass the rows down and let the client component run useFixture)" : ""}`);
240
264
  }
241
- if (!L.count) ok("examples", "fixtures stay in code (useFixture), nothing INSERTs one");
265
+ if (!L.count) ok("examples", "examples show only outside a workspace (useFixture); a workspace app opens empty");
242
266
  }
243
267
 
244
268
  // 4. The manifest names surfaces that exist, once each, under keys that are free.
@@ -270,7 +294,10 @@ const surfaceKeys = [];
270
294
  }
271
295
 
272
296
  // 5. Every icon named exists in the sprite (a depiction's own glyphs are
273
- // not sprite names).
297
+ // not sprite names). The sprite is a kit file, fixed like the rest (the
298
+ // next rule refuses an edit to it), so the way out is another name —
299
+ // until 0.3.3 this line said "Add the glyph there", a door the next
300
+ // rule shuts.
274
301
  {
275
302
  const sprite = readFileSync(join(ROOT, "components/ui/icon.jsx"), "utf8");
276
303
  const names = new Set([...sprite.matchAll(/^\s+"?([a-z][a-z0-9-]*)"?:\s*"/gm)].map((m) => m[1]));
@@ -282,7 +309,7 @@ const surfaceKeys = [];
282
309
  scan(/\bicon(?:=|:\s*)["']([a-z][a-z0-9-]*)["']/, (f, m, line) => check(f, m[1], line));
283
310
  if (missing.size) {
284
311
  const where = [...missing].map(([n, at]) => `"${n}" (${at})`).join(", ");
285
- fail("icons", "sprite", `not in components/ui/icon.jsx: ${where}. Add the glyph there, or pick one of: ${[...names].join(" ")}`);
312
+ fail("icons", "sprite", `not in components/ui/icon.jsx: ${where}. The kit is fixed, so pick one of: ${[...names].join(" ")}`);
286
313
  } else ok("icons", "every icon name exists");
287
314
  }
288
315
 
@@ -531,12 +558,12 @@ const surfaceKeys = [];
531
558
 
532
559
  // 6. Smoke: the app, built and started, answers every surface.
533
560
 
534
- /** Newest mtime among the app's own sources (what a build reads). */
561
+ /** Newest mtime among the app's own sources (what a build reads): walk
562
+ * leaves the build folders out, and check/ is not built. */
535
563
  function newestSource() {
536
564
  let newest = 0;
537
565
  for (const f of walk(ROOT)) {
538
- const rel = relative(ROOT, f);
539
- if (/^(node_modules|\.next|\.next-check|check)\//.test(rel)) continue;
566
+ if (/^check\//.test(relative(ROOT, f))) continue;
540
567
  newest = Math.max(newest, statSync(f).mtimeMs);
541
568
  }
542
569
  return newest;
@@ -571,7 +598,7 @@ function copySources(dir) {
571
598
  dereference: false,
572
599
  filter: (src) => {
573
600
  const rel = relative(ROOT, src);
574
- return !/^(node_modules|\.next|\.next-check|check)(\/|$)/.test(rel);
601
+ return !notSource(rel) && !/^check(\/|$)/.test(rel);
575
602
  },
576
603
  });
577
604
  const link = join(dir, "node_modules");
@@ -625,6 +652,12 @@ async function smoke() {
625
652
  const home = await fetch(`${origin}/`, { redirect: "manual" });
626
653
  results.push(`/ → ${home.status}`);
627
654
  if (!(home.status === 200 || (home.status >= 300 && home.status < 400))) fail("smoke", "/", `answered ${home.status}`);
655
+ // The route app/auto-refresh.jsx asks before every refresh. Anything
656
+ // but its 204 and the app never refreshes again: its rows go stale
657
+ // while the agent writes them, and nothing on the page says so.
658
+ const alive = await fetch(`${origin}/claw-alive`, { redirect: "manual" });
659
+ results.push(`/claw-alive → ${alive.status}`);
660
+ if (alive.status !== 204) fail("smoke", "/claw-alive", `answered ${alive.status}, not 204: app/auto-refresh.jsx will never refresh this app. \`makersclaw update\` restores app/claw-alive/route.js`);
628
661
  for (const key of surfaceKeys) {
629
662
  const res = await fetch(`${origin}/s/${encodeURIComponent(key)}`);
630
663
  const body = await res.text();
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
 
3
- import { useMemo, useState } from "react";
3
+ import { createContext, useContext, useMemo, useState } from "react";
4
4
  import { markSample } from "./sample";
5
5
  import { Button, Notice } from "../ui";
6
6
 
@@ -20,18 +20,43 @@ import { Button, Notice } from "../ui";
20
20
  //
21
21
  // `rows` keeps its identity between renders (an effect may depend on it);
22
22
  // declare SAMPLE at module level, not inside the component, so it does too.
23
+ //
24
+ // Examples show only OUTSIDE a Makersclaw workspace: the store's authoring
25
+ // loop, a shadcn project. Inside one, an app never shows example data (up
26
+ // to 0.3.x it did, until its table had a row): an empty table stays empty
27
+ // and the screen shows what its agent is doing instead. app/layout.jsx
28
+ // decides it per request and says so through <Examples allowed>; with no
29
+ // provider at all — a shadcn project — the answer is yes. The provider
30
+ // lives here, not in lib/, so the parts still travel as one folder.
23
31
 
24
32
  const NONE = [];
33
+ const nothing = () => {};
34
+
35
+ const ExamplesContext = createContext(true);
36
+
37
+ /** Whether the parts below may show an example. The layout wraps the app
38
+ * in it; `allowed` is false inside a workspace. */
39
+ export function Examples({ allowed, children }) {
40
+ return <ExamplesContext.Provider value={Boolean(allowed)}>{children}</ExamplesContext.Provider>;
41
+ }
42
+
43
+ /** true unless an <Examples allowed={false}> is above. */
44
+ export function useExamples() {
45
+ return useContext(ExamplesContext);
46
+ }
25
47
 
26
48
  export function useFixture(rows, sample, { idColumn = "id" } = {}) {
49
+ const allowed = useExamples();
27
50
  const [cleared, setCleared] = useState(false);
28
- const isFixture = !cleared && (!rows || rows.length === 0) && Array.isArray(sample) && sample.length > 0;
51
+ const isFixture = allowed && !cleared && (!rows || rows.length === 0) && Array.isArray(sample) && sample.length > 0;
29
52
  const fixtureRows = useMemo(() => markSample(sample, idColumn), [sample, idColumn]);
53
+ // With examples off there is no example to clear or bring back: the
54
+ // real rows, however few, are all a surface gets.
30
55
  return {
31
56
  rows: isFixture ? fixtureRows : rows ?? NONE,
32
57
  isFixture,
33
- clear: () => setCleared(true),
34
- restore: () => setCleared(false),
58
+ clear: allowed ? () => setCleared(true) : nothing,
59
+ restore: allowed ? () => setCleared(false) : nothing,
35
60
  };
36
61
  }
37
62
 
@@ -1,5 +1,6 @@
1
1
  // Parts: the kit's pieces bound to this template's data layer. Each
2
2
  // exists because an app got it wrong once; each carries the behaviour
3
3
  // that makes it right. README §3b.
4
- export { useFixture, isSample, FixtureNotice } from "./fixture";
4
+ export { Examples, useExamples, useFixture, isSample, FixtureNotice } from "./fixture";
5
+ export { LiveWork, LiveEmpty, liveWords } from "./live-work";
5
6
  export { RecordSwitcher } from "./record-switcher";
@@ -0,0 +1,235 @@
1
+ "use client";
2
+
3
+ import { isValidElement, useState } from "react";
4
+ import { Button, Icon, StatusDot, cx } from "../ui";
5
+
6
+ // The empty state that shows the work coming, never an example. Inside a
7
+ // Makersclaw workspace a table opens empty (fixture.jsx: examples show
8
+ // only outside one), and a table is usually empty because its agent has
9
+ // not got to it yet — or is on it right now. So the empty block says
10
+ // which: the ask in flight on this surface (its line, whose ask it is,
11
+ // what it waits behind, the one press it needs), or, with nothing in
12
+ // flight, what would appear here and the one way to make it. Up to 0.3.x
13
+ // an example filled that gap; since 0.4.0 no app shows example data in a
14
+ // workspace (claw's docs/JARVIS-PLATFORM.md §9.6 and §12, promise 6).
15
+ //
16
+ // const work = useRequests({ surface: "deals" }); // lib/hooks.js
17
+ // if (!rows.length) {
18
+ // return <LiveEmpty work={work} surface="deals" icon="inbox" title="No deals yet"
19
+ // line="Ask the agent to find some, or add one." action={{ label: "Add a deal", onClick: add }} />;
20
+ // }
21
+ //
22
+ // Props in, callbacks out: `work` is useRequests()'s answer as it stands,
23
+ // and the row calls back through it — work.open(request) opens the
24
+ // conversation in the desk's agent column (where a need is answered),
25
+ // work.stop(id) stops an ask. The part imports nothing from lib/, so the
26
+ // parts still travel as one folder; in a shadcn project, with no `work`,
27
+ // it is the plain empty block.
28
+
29
+ // A request's status is one of eleven (claw's shared/src/requests.ts;
30
+ // lib/hooks.js keeps the same list — this folder may not import it).
31
+ // Every step before the agent is on it reads as Working: the member asked
32
+ // for the result, not for its plumbing.
33
+ const WORDS = new Map([
34
+ ["routing", "Working"],
35
+ ["queued", "Working"],
36
+ ["installing", "Working"],
37
+ ["setting_up", "Working"],
38
+ ["building", "Working"],
39
+ ["working", "Working"],
40
+ ["needs_you", "Needs you"],
41
+ ["answered", "Answered"],
42
+ ["done", "Done"],
43
+ ["stopped", "Stopped"],
44
+ ["didnt_finish", "Didn't finish"],
45
+ ]);
46
+
47
+ /** The seven statuses in which the work still goes. `answered` is open
48
+ * but not live: the agent answered and waits, and no row is coming. */
49
+ const LIVE = new Set(["routing", "queued", "installing", "setting_up", "building", "working", "needs_you"]);
50
+
51
+ /** A scheduled run's statuses while it goes (a brief's, not a request's:
52
+ * _claw/requests returns the run only in one of these). */
53
+ const RUN_LIVE = new Set(["queued", "planning", "awaiting_approval", "in_progress"]);
54
+
55
+ /** The status word for a request or a bare status: "Working", "Needs
56
+ * you", "Answered", "Done", "Stopped", "Didn't finish". An unknown
57
+ * status is "" — a word the platform has not taught this part yet is
58
+ * better unsaid than guessed. */
59
+ export function liveWords(requestOrStatus) {
60
+ const status = typeof requestOrStatus === "string" ? requestOrStatus : requestOrStatus?.status;
61
+ return WORDS.get(status) ?? "";
62
+ }
63
+
64
+ function runGoing(run) {
65
+ return Boolean(run && typeof run === "object" && (run.status == null || RUN_LIVE.has(run.status)));
66
+ }
67
+
68
+ /**
69
+ * One piece of work in flight, as a row: a pulsing dot, the request's
70
+ * line, whose request it is when it is not yours, what it waits behind.
71
+ * A need gets the warning tone and Answer (it is answered in the desk's
72
+ * column, where the conversation lives); your own ask gets Stop. With no
73
+ * request, a scheduled run that is going shows its title.
74
+ *
75
+ * request one of useRequests().live (lib/hooks.js)
76
+ * run useRequests().run, shown when there is no request
77
+ * onOpen (request) → open its conversation; Answer needs it
78
+ * onStop (id) → stop it; Stop needs it, and shows only to the asker
79
+ */
80
+ export function LiveWork({ request, run, onOpen, onStop, className }) {
81
+ const [stopping, setStopping] = useState(false);
82
+
83
+ if (request && typeof request === "object") {
84
+ const status = request.status;
85
+ const word = liveWords(request);
86
+ const needs = status === "needs_you";
87
+ const queued = status === "queued";
88
+ const theirs = request.mine === false;
89
+ const main = request.line || word || request.title || "";
90
+ const meta = [];
91
+ if (theirs) meta.push(`from ${request.asker_name || "a teammate"}'s request`);
92
+ // claw's line for a queued request already says "Queued · after …"
93
+ // (lineOf, shared/src/requests.ts); say it once, under a line that
94
+ // does not.
95
+ if (queued && !/^Queued\b/.test(main)) meta.push(`Queued · after ${request.queued?.after_title || "the job before it"}`);
96
+ // Only the asker answers: a teammate's need opens on "Waiting on …"
97
+ // in the desk, so it is shown, not offered.
98
+ const answer = needs && !theirs && onOpen;
99
+ const going = LIVE.has(status);
100
+ const stop = !needs && going && request.mine === true && onStop;
101
+ const onStopClick = async () => {
102
+ setStopping(true);
103
+ try {
104
+ await onStop(request.id);
105
+ } finally {
106
+ setStopping(false);
107
+ }
108
+ };
109
+ return (
110
+ <Row
111
+ tone={needs ? "warning" : going && !queued ? "primary" : "neutral"}
112
+ pulse={going && !queued}
113
+ word={word}
114
+ warn={needs}
115
+ main={main}
116
+ meta={meta}
117
+ className={className}
118
+ >
119
+ {answer ? (
120
+ <Button size="sm" variant="primary" onClick={() => onOpen(request)}>
121
+ Answer
122
+ </Button>
123
+ ) : stop ? (
124
+ <Button size="sm" variant="quiet" loading={stopping} onClick={onStopClick}>
125
+ Stop
126
+ </Button>
127
+ ) : null}
128
+ </Row>
129
+ );
130
+ }
131
+
132
+ if (runGoing(run)) {
133
+ return (
134
+ <Row tone="primary" pulse word="Working" main={run.title || "Working"} meta={run.title ? ["Working"] : []} className={className} />
135
+ );
136
+ }
137
+ return null;
138
+ }
139
+
140
+ function Row({ tone, pulse, word, warn, main, meta, className, children }) {
141
+ return (
142
+ <div
143
+ className={cx(
144
+ "flex items-center gap-3 rounded-lg border bg-card px-3 py-2.5 text-left text-sm text-card-foreground",
145
+ warn ? "border-warning/50" : "border-border",
146
+ className,
147
+ )}
148
+ >
149
+ <StatusDot tone={tone} pulse={pulse} label={word || "Working"} hideLabel />
150
+ <div aria-live="polite" className="min-w-0 flex-1">
151
+ <p className="truncate font-medium">{main}</p>
152
+ {meta.length ? <p className="truncate text-xs text-muted-foreground">{meta.join(" · ")}</p> : null}
153
+ </div>
154
+ {children ? <div className="flex shrink-0 items-center gap-2">{children}</div> : null}
155
+ </div>
156
+ );
157
+ }
158
+
159
+ /** Does a request belong to this empty state? Each filter given is a way
160
+ * in (lib/hooks.js reads it the same way): an ask made on this surface,
161
+ * or work on this job wherever it was asked for. No filter: any. */
162
+ function belongs(request, surface, job) {
163
+ if (!surface && !job) return true;
164
+ return Boolean((surface && request.surface === surface) || (job && request.job_key === job));
165
+ }
166
+
167
+ // The one to show when several are in flight: what needs an answer, then
168
+ // what is working, then what waits in the queue; yours before a
169
+ // teammate's; the newest first.
170
+ const rank = (r) => (r.status === "needs_you" ? 0 : r.status === "queued" ? 2 : 1);
171
+
172
+ function pickRequest(live, surface, job) {
173
+ const list = (Array.isArray(live) ? live : [])
174
+ .map((r, i) => ({ r, i }))
175
+ .filter(({ r }) => r && typeof r === "object" && LIVE.has(r.status) && belongs(r, surface, job));
176
+ list.sort(
177
+ (a, b) =>
178
+ rank(a.r) - rank(b.r) ||
179
+ Number(b.r.mine === true) - Number(a.r.mine === true) ||
180
+ String(b.r.created_at ?? "").localeCompare(String(a.r.created_at ?? "")) ||
181
+ a.i - b.i,
182
+ );
183
+ return list[0]?.r ?? null;
184
+ }
185
+
186
+ const FRAME = "grid place-items-center rounded-lg border border-dashed border-border px-6 py-10 text-center";
187
+
188
+ /**
189
+ * What an empty collection shows in a workspace. With work in flight for
190
+ * this surface or job — or the app's scheduled run going — the work, as
191
+ * a LiveWork row, and "Rows appear here as they land". With none, the
192
+ * empty block: icon, title, line and the one action that fills it.
193
+ *
194
+ * work useRequests()'s answer ({ live, run, open, stop }); absent
195
+ * in a shadcn project, where only the empty block shows
196
+ * surface the surface key its asks carry; job — the job key whose
197
+ * work lands here. Either is a way in; neither: any live work
198
+ * icon the empty block's glyph (inbox)
199
+ * title what would be here: "No deals yet"
200
+ * line how it gets here: "Ask the agent to find some, or add one."
201
+ * action { label, onClick, href, icon, variant } for a Button, or an
202
+ * element as it is
203
+ * children anything the surface adds under either state
204
+ */
205
+ export function LiveEmpty({ work, surface, job, icon = "inbox", title, line, action, children, className }) {
206
+ const request = pickRequest(work?.live, surface, job);
207
+ const run = !request && runGoing(work?.run) ? work.run : null;
208
+
209
+ if (request || run) {
210
+ return (
211
+ <div className={cx(FRAME, "gap-3", className)}>
212
+ <LiveWork request={request} run={run} onOpen={work.open} onStop={work.stop} className="w-full max-w-md" />
213
+ <p className="text-sm text-muted-foreground">Rows appear here as they land</p>
214
+ {children}
215
+ </div>
216
+ );
217
+ }
218
+
219
+ const button = isValidElement(action) ? (
220
+ action
221
+ ) : action && typeof action === "object" && action.label ? (
222
+ <Button variant={action.variant ?? "secondary"} icon={action.icon} href={action.href} onClick={action.onClick}>
223
+ {action.label}
224
+ </Button>
225
+ ) : null;
226
+ return (
227
+ <div className={cx(FRAME, "gap-2", className)}>
228
+ <Icon name={icon} size={22} className="text-muted-foreground" />
229
+ {title ? <p className="text-sm font-medium">{title}</p> : null}
230
+ {line ? <p className="max-w-prose text-sm text-muted-foreground">{line}</p> : null}
231
+ {button ? <div className="mt-2">{button}</div> : null}
232
+ {children}
233
+ </div>
234
+ );
235
+ }
@@ -9,7 +9,12 @@ import { Spinner } from "./feedback";
9
9
  // by hand in a surface: the look is the workspace's, the behaviour is
10
10
  // the browser's, and this is the only place both meet.
11
11
  //
12
- // variant primary (the one main action per surface) · secondary ·
12
+ // variant primary (the one main action per surface, filled with
13
+ // --primary, the app's accent) · secondary (the default, the
14
+ // workspace's neutral --secondary: a Button that does not say
15
+ // primary is never the accent — a surface's "Add guest"
16
+ // written without variant="primary" came out dark grey on a
17
+ // coral app, 2026-09-25) ·
13
18
  // quiet (no fill) · danger (destructive, red text) ·
14
19
  // link (text only: inline, no box, padding or min-height — it
15
20
  // wraps and truncates with the sentence it sits in) · nav (a navigation row: quiet, full
@@ -139,6 +139,30 @@ export function formatDate(s, options = { dateStyle: "medium" }, locale) {
139
139
  return d ? plainSpaces(new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" }).format(d)) : "";
140
140
  }
141
141
 
142
+ /** Any date a row holds → a Date to show in the reader's own time; null
143
+ * when it is none. A day ("2026-09-28", what a `date` column reads as)
144
+ * is that day's LOCAL midnight: new Date("2026-09-28") is UTC's
145
+ * midnight, the 27th everywhere west of Greenwich, and a screen that
146
+ * shows it, then saves what it shows, moves the day back by one. A Date
147
+ * stays as it is; an ISO timestamp or epoch milliseconds go through
148
+ * new Date(). For a day alone, formatDate and the helpers above need no
149
+ * Date at all; this is for code that mixes days with instants (a
150
+ * comment's time, "due" beside "sent at"). */
151
+ export function toLocalDate(value) {
152
+ if (value === null || value === undefined || value === "") return null;
153
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? null : value;
154
+ const day = typeof value === "string" ? parseDate(value) : null;
155
+ if (day) {
156
+ // setFullYear, not new Date(y, m, d): that one reads years 0–99 as 19xx.
157
+ const local = new Date(0);
158
+ local.setFullYear(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate());
159
+ local.setHours(0, 0, 0, 0);
160
+ return local;
161
+ }
162
+ const d = new Date(value);
163
+ return Number.isNaN(d.getTime()) ? null : d;
164
+ }
165
+
142
166
  // The week's first day for a locale: Intl's own answer where the browser
143
167
  // has it (getWeekInfo), else CLDR's regions that start on Sunday or
144
168
  // Saturday; everyone else starts on Monday.
@@ -275,6 +275,15 @@ export function DatePicker({
275
275
  * value, onChange(date | null), defaultValue, min, max, disabled
276
276
  * (true, or a function for the Calendar's days), name, invalid, size,
277
277
  * locale (the Calendar's; the field itself follows the browser), today
278
+ * clearable an × beside the calendar button once the field holds a
279
+ * date (false — DatePicker's is true)
280
+ *
281
+ * Every other prop goes onto the <input>, so a prop this part names must
282
+ * be taken out here: until 0.3.3 `clearable` was DatePicker's alone, and
283
+ * a surface that wrote <DateInput clearable> put it on the <input> ("Received
284
+ * `true` for a non-boolean attribute clearable", a podcast guest sheet,
285
+ * 2026-09-25). It is DateInput's now too, off unless asked, so a field
286
+ * that did not ask keeps its width.
278
287
  */
279
288
  export function DateInput({
280
289
  id,
@@ -290,6 +299,7 @@ export function DateInput({
290
299
  locale,
291
300
  today,
292
301
  weekStartsOn,
302
+ clearable = false,
293
303
  className,
294
304
  ...rest
295
305
  }) {
@@ -299,6 +309,7 @@ export function DateInput({
299
309
  const [month, setMonth] = useState(null);
300
310
  const off = disabled === true;
301
311
  const out = !!value && ((min && value < min) || (max && value > max));
312
+ const showClear = clearable && !off && !!value;
302
313
 
303
314
  function set(v) {
304
315
  if (valueProp === undefined) setOwn(v);
@@ -318,12 +329,14 @@ export function DateInput({
318
329
  invalid={invalid || out}
319
330
  onChange={(e) => set(e.target.value || null)}
320
331
  className={cx(
321
- "pr-12 [&::-webkit-calendar-picker-indicator]:hidden tabular-nums",
332
+ showClear ? "pr-20" : "pr-12",
333
+ "[&::-webkit-calendar-picker-indicator]:hidden tabular-nums",
322
334
  size === "sm" && "min-h-9",
323
335
  )}
324
336
  {...rest}
325
337
  />
326
- <span className={cx("absolute right-1", size === "sm" ? "top-0" : "top-1")}>
338
+ <span className={cx("absolute right-1 flex", size === "sm" ? "top-0" : "top-1")}>
339
+ {showClear ? <IconButton icon="x" label="Clear date" size="sm" onClick={() => set(null)} /> : null}
327
340
  <Popover
328
341
  open={open}
329
342
  onOpenChange={(next) => {
@@ -8,10 +8,19 @@ import { cx } from "./cx";
8
8
  // validation, autofill). Wrap one in <Field> for its label, hint and
9
9
  // error — never a bare <input> with a <div> label beside it.
10
10
 
11
- const CONTROL =
12
- "w-full min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground " +
11
+ const BOX =
12
+ "min-h-11 rounded-md border border-input bg-background px-3 text-sm text-foreground " +
13
13
  "placeholder:text-muted-foreground outline-none focus-visible:ring-2 focus-visible:ring-ring " +
14
14
  "disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:border-destructive";
15
+ const CONTROL = `w-full ${BOX}`;
16
+
17
+ // An Input is as wide as its column unless the caller's className gives
18
+ // it a width (w-24, w-auto, w-[12rem], size-10), which then stands in
19
+ // for w-full. cx does not merge classes: with both, the rule later in the
20
+ // stylesheet wins, and Tailwind writes w-full after w-24, w-auto, w-fit
21
+ // and w-[…] — until 0.5.1 those did nothing on an Input. A width under a
22
+ // breakpoint (sm:w-40) keeps w-full below it.
23
+ const ownWidth = (className) => /(?:^|\s)!?(?:w|size)-/.test(className ?? "");
15
24
 
16
25
  /** Label, control, hint and error in one stack. Pass `id`; the label
17
26
  * points at it. `error` (a string) marks the control invalid. */
@@ -39,7 +48,7 @@ export function Field({ id, label, hint, error, required, className, children })
39
48
  }
40
49
 
41
50
  export function Input({ className, invalid, ...rest }) {
42
- return <input className={cx(CONTROL, className)} aria-invalid={invalid ? true : undefined} {...rest} />;
51
+ return <input className={cx(!ownWidth(className) && "w-full", BOX, className)} aria-invalid={invalid ? true : undefined} {...rest} />;
43
52
  }
44
53
 
45
54
  /**
@@ -3,6 +3,18 @@
3
3
  // one here, never an <svg> in a surface. `name` is a key below; unknown
4
4
  // names render nothing (and `npm run check` names them).
5
5
  //
6
+ // `size` is pixels (16, the default) or one of the kit's size names:
7
+ // xs 12 · sm 14 · md 16 · lg 20 · xl 24. Every other part of the kit
8
+ // takes size="sm", so a surface writes it here too — and until 0.3.3 a
9
+ // name went onto the <svg> as width="sm", which is not a length: the
10
+ // browser logged "Expected length, 'sm'" and drew the glyph at its
11
+ // 300×150 default, a calendar as big as the card around it (a podcast
12
+ // guest board, 2026-09-25).
13
+ //
14
+ // A glyph added for a build takes lucide's name when lucide has one
15
+ // (calendar-check, trash-2), because that is the name a build reaches for
16
+ // first; the path is our own, drawn to the same 24 box, never copied.
17
+ //
6
18
  // BRANDS holds the brand marks: google, github, apple, microsoft,
7
19
  // x-logo (the X / Twitter mark; `x` is the close glyph), linkedin,
8
20
  // instagram, youtube, facebook. Unlike PATHS they are FILLED shapes
@@ -167,6 +179,13 @@ const PATHS = {
167
179
  kanban: "M4 4h4v14H4zM10 4h4v9h-4zM16 4h4v16h-4z",
168
180
  table: "M3 4h18v16H3zM3 10h18M3 15h18M9 10v10",
169
181
  gantt: "M4 4v16h16M8 8h6M11 12h7M9 16h5",
182
+ // Names builds reached for that the sprite lacked, so `npm run check`
183
+ // refused the ship and the build spent a round renaming them (the
184
+ // launch build test, 2026-09-25, and the local build logs before it):
185
+ // a day that is booked, the bin with its slats, a small tree of nodes.
186
+ "calendar-check": "M3 5h18v16H3zM16 3v4M8 3v4M3 10h18M9 15.5l2 2 4-4",
187
+ "trash-2": "M3 6h18M8 6V4h8v2m-9 0 1 14h8l1-14M10 11v6M14 11v6",
188
+ network: "M9 2h6v6H9zM2 16h6v6H2zM16 16h6v6h-6zM12 8v4M5 16v-4h14v4",
170
189
  };
171
190
 
172
191
  const BRANDS = {
@@ -183,14 +202,30 @@ const BRANDS = {
183
202
 
184
203
  export const ICON_NAMES = [...Object.keys(PATHS), ...Object.keys(BRANDS)];
185
204
 
205
+ // The kit's size names in pixels. A number, or a length the <svg> can
206
+ // read ("18", "1.25em"), is kept as it was; anything else is the default,
207
+ // never an attribute the browser cannot read.
208
+ const SIZES = { xs: 12, sm: 14, md: 16, lg: 20, xl: 24 };
209
+ const LENGTH = /^(\d+\.?\d*|\.\d+)(px|em|rem|%)?$/;
210
+
211
+ function pixels(size) {
212
+ if (typeof size === "number" && size > 0) return size;
213
+ if (typeof size === "string") {
214
+ if (SIZES[size]) return SIZES[size];
215
+ if (LENGTH.test(size)) return size;
216
+ }
217
+ return SIZES.md;
218
+ }
219
+
186
220
  export function Icon({ name, size = 16, className, label }) {
187
221
  const brand = BRANDS[name];
188
222
  const d = PATHS[name] || brand;
189
223
  if (!d) return null;
224
+ const px = pixels(size);
190
225
  return (
191
226
  <svg
192
- width={size}
193
- height={size}
227
+ width={px}
228
+ height={px}
194
229
  viewBox="0 0 24 24"
195
230
  fill={brand ? "currentColor" : "none"}
196
231
  fillRule={brand ? "evenodd" : undefined}
@@ -23,6 +23,7 @@ export {
23
23
  endOfMonth,
24
24
  dayOfWeek,
25
25
  formatDate,
26
+ toLocalDate,
26
27
  firstDayOfWeek,
27
28
  } from "./calendar";
28
29
  export { DatePicker, DateInput, DATE_PRESETS } from "./date-picker";