makersclaw 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +56 -9
  2. package/package.json +1 -1
  3. package/r/annotate.json +4 -4
  4. package/r/approval-request.json +3 -3
  5. package/r/audio-player.json +3 -3
  6. package/r/auth-form.json +2 -2
  7. package/r/board.json +4 -4
  8. package/r/calendar-view.json +4 -4
  9. package/r/cart.json +3 -3
  10. package/r/chart.json +6 -6
  11. package/r/chat.json +3 -3
  12. package/r/code-editor.json +4 -4
  13. package/r/comment-box.json +4 -4
  14. package/r/contact-section.json +3 -3
  15. package/r/core.json +10 -4
  16. package/r/csv-import.json +4 -4
  17. package/r/data-table.json +3 -3
  18. package/r/diagram.json +3 -3
  19. package/r/doc-editor.json +4 -4
  20. package/r/doc-page.json +4 -4
  21. package/r/email-render.json +2 -2
  22. package/r/faq.json +2 -2
  23. package/r/file-upload.json +3 -3
  24. package/r/flow-canvas.json +4 -4
  25. package/r/footer.json +3 -3
  26. package/r/gantt.json +4 -4
  27. package/r/job-panel.json +3 -3
  28. package/r/json-editor.json +4 -4
  29. package/r/map.json +5 -5
  30. package/r/menubar.json +2 -2
  31. package/r/mind-map.json +4 -4
  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 +6 -6
  36. package/r/payment-methods.json +2 -2
  37. package/r/pdf-document.json +2 -2
  38. package/r/pdf-viewer.json +6 -6
  39. package/r/product-detail.json +3 -3
  40. package/r/product-grid.json +4 -4
  41. package/r/record-form.json +3 -3
  42. package/r/record-notes.json +5 -5
  43. package/r/registry.json +126 -121
  44. package/r/search-dialog.json +3 -3
  45. package/r/settings-list.json +3 -3
  46. package/r/setup-checklist.json +3 -3
  47. package/r/share-dialog.json +3 -3
  48. package/r/sidebar.json +3 -3
  49. package/r/signature-pad.json +3 -3
  50. package/r/sketch-canvas.json +5 -5
  51. package/r/spreadsheet.json +3 -3
  52. package/r/stat-cards.json +4 -4
  53. package/r/task-list.json +4 -4
  54. package/r/task-model.json +3 -3
  55. package/r/testimonials.json +2 -2
  56. package/r/transaction-list.json +3 -3
  57. package/r/whiteboard.json +4 -4
  58. package/registry/blocks/annotate/README.md +24 -12
  59. package/registry/blocks/approval-request/README.md +15 -8
  60. package/registry/blocks/audio-player/README.md +17 -10
  61. package/registry/blocks/board/README.md +13 -9
  62. package/registry/blocks/board/board.jsx +1 -1
  63. package/registry/blocks/calendar-view/README.md +28 -4
  64. package/registry/blocks/cart/README.md +14 -10
  65. package/registry/blocks/chart/README.md +14 -7
  66. package/registry/blocks/chart/cartesian.jsx +1 -1
  67. package/registry/blocks/chart/core.jsx +33 -23
  68. package/registry/blocks/chart/pie.jsx +1 -1
  69. package/registry/blocks/chat/README.md +47 -14
  70. package/registry/blocks/chat/demo.jsx +2 -2
  71. package/registry/blocks/code-editor/README.md +4 -0
  72. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  73. package/registry/blocks/comment-box/README.md +20 -13
  74. package/registry/blocks/contact-section/README.md +7 -6
  75. package/registry/blocks/csv-import/README.md +34 -12
  76. package/registry/blocks/data-table/README.md +41 -8
  77. package/registry/blocks/diagram/diagram.jsx +1 -1
  78. package/registry/blocks/doc-editor/README.md +18 -6
  79. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  80. package/registry/blocks/doc-page/README.md +16 -6
  81. package/registry/blocks/file-upload/README.md +7 -1
  82. package/registry/blocks/flow-canvas/README.md +30 -12
  83. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  84. package/registry/blocks/footer/README.md +7 -4
  85. package/registry/blocks/gantt/README.md +18 -12
  86. package/registry/blocks/job-panel/README.md +1 -1
  87. package/registry/blocks/json-editor/README.md +16 -4
  88. package/registry/blocks/map/README.md +19 -3
  89. package/registry/blocks/map/map-view.jsx +1 -1
  90. package/registry/blocks/map/maplibre.js +1 -1
  91. package/registry/blocks/mind-map/README.md +20 -7
  92. package/registry/blocks/org-chart/README.md +14 -9
  93. package/registry/blocks/page-builder/README.md +14 -7
  94. package/registry/blocks/page-builder/demo.jsx +3 -3
  95. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  96. package/registry/blocks/page-builder/page-view.jsx +1 -1
  97. package/registry/blocks/page-builder/page.js +1 -1
  98. package/registry/blocks/pdf-viewer/README.md +26 -21
  99. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  100. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  101. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  102. package/registry/blocks/product-detail/README.md +13 -7
  103. package/registry/blocks/product-detail/demo.jsx +1 -1
  104. package/registry/blocks/product-grid/README.md +12 -10
  105. package/registry/blocks/record-form/README.md +26 -9
  106. package/registry/blocks/record-notes/README.md +42 -30
  107. package/registry/blocks/search-dialog/README.md +38 -7
  108. package/registry/blocks/settings-list/README.md +29 -17
  109. package/registry/blocks/setup-checklist/README.md +12 -14
  110. package/registry/blocks/share-dialog/README.md +1 -1
  111. package/registry/blocks/sidebar/README.md +23 -13
  112. package/registry/blocks/signature-pad/README.md +15 -4
  113. package/registry/blocks/sketch-canvas/README.md +25 -10
  114. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  115. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  116. package/registry/blocks/spreadsheet/README.md +17 -8
  117. package/registry/blocks/stat-cards/README.md +17 -8
  118. package/registry/blocks/task-list/README.md +14 -11
  119. package/registry/blocks/task-model/README.md +3 -3
  120. package/registry/blocks/transaction-list/README.md +15 -7
  121. package/registry/blocks/whiteboard/README.md +34 -22
  122. package/src/cli.mjs +33 -6
  123. package/src/install.mjs +285 -26
  124. package/src/registry.mjs +109 -1
  125. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  126. package/templates/next/browser/app/layout.jsx +94 -0
  127. package/templates/next/browser/app/page.jsx +60 -0
  128. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  129. package/templates/next/browser/lib/actions.js +16 -0
  130. package/templates/next/browser/lib/data.js +43 -0
  131. package/templates/next/files/README.md +1100 -89
  132. package/templates/next/files/app/frame.js +128 -4
  133. package/templates/next/files/app/shell.jsx +54 -37
  134. package/templates/next/files/check/run.mjs +1514 -45
  135. package/templates/next/files/components/ui/index.js +1 -0
  136. package/templates/next/files/components/ui/item.jsx +2 -2
  137. package/templates/next/files/components/ui/layout.jsx +307 -0
  138. package/templates/next/files/components/ui/notice.jsx +1 -1
  139. package/templates/next/files/components/ui/separator.jsx +1 -1
  140. package/templates/next/files/lib/actions.js +170 -51
  141. package/templates/next/files/lib/brain-server.js +96 -0
  142. package/templates/next/files/lib/brain.js +413 -0
  143. package/templates/next/files/lib/claw.js +15 -1
  144. package/templates/next/files/lib/data.js +119 -29
  145. package/templates/next/files/lib/db.js +97 -17
  146. package/templates/next/files/lib/door.js +680 -0
  147. package/templates/next/files/lib/hooks.js +71 -2
  148. package/templates/next/files/lib/platform.js +93 -4
  149. package/templates/next/files/next.config.mjs +36 -2
  150. package/templates/next/files/surfaces.js +3 -2
  151. package/templates/next/template.json +18 -3
  152. package/templates/next/files/schema.sql +0 -22
package/r/task-list.json CHANGED
@@ -5,8 +5,8 @@
5
5
  "title": "Task list",
6
6
  "description": "The List view of work, like ClickUp's or Linear's: tasks under headings that fold (group by status, assignee, priority, project, due date or any field), a round check to complete each, the title edited in place, subtasks indented under their parent with their own fold, the fields you choose (assignees, due date, priority, tags, project, estimate) as inline pickers, a quick-add row at the end of every group, drag to reorder and to move between groups (saving the new status, assignee or due date), multi-select with a bulk bar (status, assign, due, priority, delete with undo), a toolbar with search, filter chips, group by, sort and fields, and friendly empty states with starter templates. One async onChange saves every change; failures roll back with a notice.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/task-model.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/task-model.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -26,7 +26,7 @@
26
26
  "path": "registry/blocks/task-list/README.md",
27
27
  "type": "registry:file",
28
28
  "target": "@components/makersclaw/blocks/task-list/README.md",
29
- "content": "# Task list\n\nThe List view of work. Tasks sit under headings that fold — by status,\nassignee, priority, project, due date or any field. Each row has a round\ncheck to complete it, a title you click to rename in place, its subtasks\nindented under it (with their own fold and a \"2/5\" count), and the fields\nyou choose — people, due date, priority, tags, project, estimate — as\npickers right in the row. Every group ends in \"Add task\": type, Enter, and\nthe next one is ready. Drag a row by its grip to reorder it, or into\nanother group to change its status, assignee or due date. Tick several\n(the row menu's \"Select\", or x on a focused title, shift for a range) and\na bar sets their status, assignee, due date or priority together, or\ndeletes them — with Undo in the toast. Search, filters (as chips), group,\nsort and fields live in the toolbar above.\n\nEvery change saves through one async `onChange(change)` and shows at\nonce; when the save fails the list puts it back and says why.\n\n```jsx\nimport { TaskList } from \"../components/blocks/task-list\"; // a Makersclaw app (\"./blocks/task-list\" from components/)\nimport { TaskList } from \"@/components/makersclaw/blocks/task-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the tasks in the server surface (README §4). It is built on\n`task-model` (added with it): the task shape, the change shapes and the\nhelpers are documented there.\n\n## The data\n\nTasks in task-model's shape — `{ id, title, status, assignees, due?,\npriority?, parentId?, tags, project?, estimate?, order?, … }` — with the\nstatuses, people and projects they refer to. A task with `parentId` is a\nsubtask, drawn under its parent when both are in the same group (else in\nits own group with \"in Parent\" beside it). `order` is the hand-made order\ndrag-to-reorder saves; tasks without one keep the order given.\n\n## Props\n\n- `tasks` — the tasks. Pass `useFixture(rows, SAMPLE).rows` and the\n examples change on screen and save nothing (they are not rows).\n- `statuses` — `[{ id, label, tone?, hue?, done? }]` in order; `done`\n marks what the check completes to. Default: To do, In progress, In\n review, Done.\n- `people` — `[{ id, name, src? }]`: who can be assigned.\n- `projects` — `[{ id, name, hue? }]`.\n- `fields` — custom fields (`{ key, label, kind, options? }`), grouped and\n filtered like the built-in ones.\n- `onChange(change)` — save a change; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed). A change is\n task-model's `create` / `update` / `delete` / `batch`; in a Makersclaw\n app, loop over `changeWrites(change)` into insertRow / updateRow /\n deleteRow (task-model README, \"A change\"). Without `onChange` the list\n only shows: no checks, pickers, drag or quick-add.\n- `onOpen(task)` — an \"Open\" button on each row (on hover; in the row's\n menu on a phone) and in the menu: open your `Sheet` with the task.\n Clicking a title renames it; with no `onChange`, it opens.\n- `view` · `defaultView` · `onViewChange(view)` — what the list shows:\n `{ search, filters, groupBy, sort, fields, hideDone }` (task-model's\n `DEFAULT_VIEW`: grouped by status, showing assignees, due and priority).\n Pass `defaultView` to start somewhere else; pass `view` + `onViewChange`\n to keep it yourself (saved views, the URL).\n- `today` — `\"YYYY-MM-DD\"`: what \"Today\" and \"Overdue\" are measured from.\n Default the browser's today; fix it in a demo.\n- `weekStartsOn` — 0 (Sunday) … 6; default Monday.\n- `toolbar` — default true; false hides search, filter, group, sort and\n fields (and the Add task button).\n- `toolbarExtra` — your controls at the toolbar's end, before \"Add task\".\n- `addLabel` — default `\"Add task\"`.\n- `starters` — templates the empty state offers:\n `[{ label: \"Product launch\", icon?: \"sparkles\", tasks: [{ title, priority?, … }] }]`.\n A press creates them all in one change.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `loading` — skeleton rows in place of the list.\n- `error` — a danger Notice above the list (a failed read).\n- `label` — what the list is called to a screen reader, default `\"Tasks\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n## What it does, precisely\n\n- Completing: the round check moves a task to the first `done` status,\n and back to the first open one.\n- Grouped by assignee (or tags), a task with two people is under both;\n dragging it from one to another swaps that one person.\n- Grouped by due date, \"Overdue\" takes no drops (the toast says to give a\n date instead); dropping on \"Tomorrow\" sets tomorrow, and a start date\n moves with it.\n- Reordering needs the manual sort (Sort: Manual). With another sort, a\n drag still moves a task to another group.\n- Deleting a task deletes its subtasks; Undo brings all of them back.\n- Keyboard: Tab reaches every control; on a title, ↑ ↓ move between\n tasks, Enter renames (Enter saves, Escape cancels), x selects (shift\n for a range). The grip picks a row up with Space; arrows move it; Space\n drops; Escape cancels.\n- On a phone the row's fields wrap under its title, the grip and menu\n stay visible, and the bulk bar sits at the bottom of the list.\n\n## Example\n\n```jsx\n// surfaces/tasks.jsx — server: load, hand over\nimport { loadRows } from \"../lib/data\";\nimport Tasks from \"../components/Tasks\";\n\nexport default async function TasksSurface() {\n const { rows, error } = await loadRows({ source: \"public.tasks\", orderBy: \"created_at asc\" });\n const people = await loadRows({ source: \"public.people\" });\n return <Tasks tasks={rows} people={people.rows} error={error} />;\n}\n```\n\nThe list orders each group by `order` itself (Sort: Manual), so the read\norder only breaks ties.\n\n```jsx\n// components/Tasks.jsx — client: the list, the task sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { Sheet } from \"./ui\";\nimport { TaskList } from \"./blocks/task-list\";\nimport { changeWrites } from \"./blocks/task-model\";\n\nconst STATUSES = [\n { id: \"todo\", label: \"To do\", tone: \"neutral\" },\n { id: \"doing\", label: \"In progress\", tone: \"info\" },\n { id: \"done\", label: \"Done\", tone: \"success\", done: true },\n];\n\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"due\", \"priority\", \"tags\", \"parentId\", \"order\"];\n\nasync function save(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r =\n w.op === \"insert\" ? await insertRow({ source: \"public.tasks\", values: w.values, detail: w.values.title })\n : w.op === \"update\" ? await updateRow({ source: \"public.tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"public.tasks\", id: w.id });\n if (r?.ok === false) return r;\n }\n return { ok: true };\n}\n\nexport default function Tasks({ tasks, people, error }) {\n const [open, setOpen] = useState(null);\n return (\n <>\n <TaskList tasks={tasks} statuses={STATUSES} people={people} onChange={save} onOpen={setOpen} error={error}\n starters={[{ label: \"Weekly planning\", tasks: [{ title: \"Review last week\" }, { title: \"Pick three priorities\" }] }]} />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>\n </>\n );\n}\n```\n\nGrouped by person, only open work, showing the project:\n\n```jsx\n<TaskList tasks={tasks} people={people} projects={projects} onChange={save}\n defaultView={{ groupBy: \"assignees\", hideDone: true, fields: [\"due\", \"priority\", \"project\"] }} />\n```\n\n## What it will not do\n\n- Edit a task's description, comments or custom fields in the row: open\n it (`onOpen`) in a Sheet.\n- Drag a subtask out of its parent: use its menu (\"Make it a task\"), or\n \"Add subtask\" on the new parent.\n- Nest deeper than three levels from the menu (the data may be deeper;\n it is drawn).\n- Virtualise thousands of rows: filter, or page them in the surface.\n- Fetch: tasks come in as props; the page re-reads after a save.\n\nA list it cannot be is your own component in `components/`, built from\ntask-model's parts, toolbar and drag primitives — not an edit to this\nblock. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add task-list` restores them.\n"
29
+ "content": "# Task list\n\nThe List view of work. Tasks sit under headings that fold — by status,\nassignee, priority, project, due date or any field. Each row has a round\ncheck to complete it, a title you click to rename in place, its subtasks\nindented under it (with their own fold and a \"2/5\" count), and the fields\nyou choose — people, due date, priority, tags, project, estimate — as\npickers right in the row. Every group ends in \"Add task\": type, Enter, and\nthe next one is ready. Drag a row by its grip to reorder it, or into\nanother group to change its status, assignee or due date. Tick several\n(the row menu's \"Select\", or x on a focused title, shift for a range) and\na bar sets their status, assignee, due date or priority together, or\ndeletes them — with Undo in the toast. Search, filters (as chips), group,\nsort and fields live in the toolbar above.\n\nEvery change saves through one async `onChange(change)` and shows at\nonce; when the save fails the list puts it back and says why.\n\n```jsx\nimport { TaskList } from \"../components/blocks/task-list\"; // a Makersclaw app (\"./blocks/task-list\" from components/)\nimport { TaskList } from \"@/components/makersclaw/blocks/task-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the tasks in the server surface (README §4). It is built on\n`task-model` (added with it): the task shape, the change shapes and the\nhelpers are documented there.\n\n## The data\n\nTasks in task-model's shape — `{ id, title, status, assignees, due?,\npriority?, parentId?, tags, project?, estimate?, order?, … }` — with the\nstatuses, people and projects they refer to. A task with `parentId` is a\nsubtask, drawn under its parent when both are in the same group (else in\nits own group with \"in Parent\" beside it). `order` is the hand-made order\ndrag-to-reorder saves; tasks without one keep the order given.\n\n## Props\n\n- `tasks` — the tasks. Pass `useFixture(rows, SAMPLE).rows` and the\n examples change on screen and save nothing (they are not rows).\n- `statuses` — `[{ id, label, tone?, hue?, done? }]` in order; `done`\n marks what the check completes to. Default: To do, In progress, In\n review, Done.\n- `people` — `[{ id, name, src? }]`: who can be assigned.\n- `projects` — `[{ id, name, hue? }]`.\n- `fields` — custom fields (`{ key, label, kind, options? }`), grouped and\n filtered like the built-in ones.\n- `onChange(change)` — save a change; return `{ ok: true }` or\n `{ ok: false, error }` (a throw counts as failed). A change is\n task-model's `create` / `update` / `delete` / `batch`; in a Makersclaw\n app, loop over `changeWrites(change)` into insertRow / updateRow /\n deleteRow (task-model README, \"A change\"). Without `onChange` the list\n only shows: no checks, pickers, drag or quick-add.\n- `onOpen(task)` — an \"Open\" button on each row (on hover; in the row's\n menu on a phone) and in the menu: open your `Sheet` with the task.\n Clicking a title renames it; with no `onChange`, it opens.\n- `view` · `defaultView` · `onViewChange(view)` — what the list shows:\n `{ search, filters, groupBy, sort, fields, hideDone }` (task-model's\n `DEFAULT_VIEW`: grouped by status, showing assignees, due and priority).\n Pass `defaultView` to start somewhere else; pass `view` + `onViewChange`\n to keep it yourself (saved views, the URL).\n- `today` — `\"YYYY-MM-DD\"`: what \"Today\" and \"Overdue\" are measured from.\n Default the browser's today; fix it in a demo.\n- `weekStartsOn` — 0 (Sunday) … 6; default Monday.\n- `toolbar` — default true; false hides search, filter, group, sort and\n fields (and the Add task button).\n- `toolbarExtra` — your controls at the toolbar's end, before \"Add task\".\n- `addLabel` — default `\"Add task\"`.\n- `starters` — templates the empty state offers:\n `[{ label: \"Product launch\", icon?: \"sparkles\", tasks: [{ title, priority?, … }] }]`.\n A press creates them all in one change.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `loading` — skeleton rows in place of the list.\n- `error` — a danger Notice above the list (a failed read).\n- `label` — what the list is called to a screen reader, default `\"Tasks\"`.\n- `className` — layout around it (width, margin). Never colour.\n\n## What it does, precisely\n\n- Completing: the round check moves a task to the first `done` status,\n and back to the first open one.\n- Grouped by assignee (or tags), a task with two people is under both;\n dragging it from one to another swaps that one person.\n- Grouped by due date, \"Overdue\" takes no drops (the toast says to give a\n date instead); dropping on \"Tomorrow\" sets tomorrow, and a start date\n moves with it.\n- Reordering needs the manual sort (Sort: Manual). With another sort, a\n drag still moves a task to another group.\n- Deleting a task deletes its subtasks; Undo brings all of them back.\n- Keyboard: Tab reaches every control; on a title, ↑ ↓ move between\n tasks, Enter renames (Enter saves, Escape cancels), x selects (shift\n for a range). The grip picks a row up with Space; arrows move it; Space\n drops; Escape cancels.\n- On a phone the row's fields wrap under its title, the grip and menu\n stay visible, and the bulk bar sits at the bottom of the list.\n\n## Example\n\n```jsx\n// surfaces/tasks.jsx — client: read, hand over\n\"use client\";\nimport { useRows } from \"../lib/data\";\nimport Tasks from \"../components/Tasks\";\n\nexport default function TasksSurface() {\n const tasks = useRows({ source: \"tasks\", orderBy: \"created_at asc\" });\n const people = useRows({ source: \"people\" });\n return <Tasks tasks={tasks.rows} people={people.rows} loading={tasks.loading || people.loading} error={tasks.error} />;\n}\n```\n\nThe list orders each group by `order` itself (Sort: Manual), so the read\norder only breaks ties.\n\n```jsx\n// components/Tasks.jsx — client: the list, the task sheet\n\"use client\";\nimport { useState } from \"react\";\nimport { deleteRow, insertRow, updateRow } from \"../lib/actions\";\nimport { Sheet } from \"./ui\";\nimport { TaskList } from \"./blocks/task-list\";\nimport { changeWrites } from \"./blocks/task-model\";\n\nconst STATUSES = [\n { id: \"todo\", label: \"To do\", tone: \"neutral\" },\n { id: \"doing\", label: \"In progress\", tone: \"info\" },\n { id: \"done\", label: \"Done\", tone: \"success\", done: true },\n];\n\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"due\", \"priority\", \"tags\", \"parentId\", \"order\"];\n\nasync function save(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r =\n w.op === \"insert\" ? await insertRow({ source: \"tasks\", values: w.values, detail: w.values.title })\n : w.op === \"update\" ? await updateRow({ source: \"tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"tasks\", id: w.id });\n if (r?.ok === false) return r;\n }\n return { ok: true };\n}\n\nexport default function Tasks({ tasks, people, loading, error }) {\n const [open, setOpen] = useState(null);\n return (\n <>\n <TaskList tasks={tasks} statuses={STATUSES} people={people} onChange={save} onOpen={setOpen} loading={loading} error={error}\n starters={[{ label: \"Weekly planning\", tasks: [{ title: \"Review last week\" }, { title: \"Pick three priorities\" }] }]} />\n <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>\n </>\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nGrouped by person, only open work, showing the project:\n\n```jsx\n<TaskList tasks={tasks} people={people} projects={projects} onChange={save}\n defaultView={{ groupBy: \"assignees\", hideDone: true, fields: [\"due\", \"priority\", \"project\"] }} />\n```\n\n## What it will not do\n\n- Edit a task's description, comments or custom fields in the row: open\n it (`onOpen`) in a Sheet.\n- Drag a subtask out of its parent: use its menu (\"Make it a task\"), or\n \"Add subtask\" on the new parent.\n- Nest deeper than three levels from the menu (the data may be deeper;\n it is drawn).\n- Virtualise thousands of rows: filter, or page them in the surface.\n- Fetch: tasks come in as props; the page re-reads after a save.\n\nA list it cannot be is your own component in `components/`, built from\ntask-model's parts, toolbar and drag primitives — not an edit to this\nblock. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add task-list` restores them.\n"
30
30
  }
31
31
  ],
32
32
  "categories": [
@@ -56,7 +56,7 @@
56
56
  "docs": "Installed to components/makersclaw/blocks/task-list. Import: import { TaskList } from \"@/components/makersclaw/blocks/task-list\". Props and an example: components/makersclaw/blocks/task-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
57
57
  "meta": {
58
58
  "makersclaw": {
59
- "version": "0.6.0",
59
+ "version": "0.8.0",
60
60
  "useWhen": "A screen that lists work to get done and moves it along: a team's tasks, a launch checklist, a sprint, a personal to-do list, onboarding steps, content to publish. Not for records that are not work (data-table), for stage columns side by side (board), or for dates on a grid (calendar)."
61
61
  }
62
62
  }
package/r/task-model.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "title": "Task model",
6
6
  "description": "The shared work-item model every task view reads: one task shape (status, assignees, start/due, estimate, progress, priority, parent, dependencies, location, tags, project, extra fields) with statuses, people and projects; pure helpers to search, filter, group by any field, sort, nest subtasks, do date math (week and month grids, working days, overlaps, time scales), sum capacity per person, follow dependencies and the critical path, and apply, undo and persist changes; the shared parts (StatusPill, AssigneeStack, DueLabel, PriorityFlag, TaskChip, CompleteToggle), the ViewToolbar (search, filter chips, group by, sort, show fields), drag primitives on @dnd-kit, a hook for optimistic saves with rollback and undo, and adapters that drive the Board and DataTable blocks from tasks.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [
11
11
  "@dnd-kit/core@6.3.1",
@@ -59,7 +59,7 @@
59
59
  "path": "registry/blocks/task-model/README.md",
60
60
  "type": "registry:file",
61
61
  "target": "@components/makersclaw/blocks/task-model/README.md",
62
- "content": "# Task model\n\nThe shared model every view of work reads: one task shape, one context\n(statuses, people, projects, custom fields, today), the arithmetic every\nview repeats (search, filter, group, sort, subtasks, dates, capacity,\ndependencies), the Change a view sends when the founder moves something,\nthe small parts every view draws the same way, the toolbar above them, and\nthe drag primitives on @dnd-kit. List, board, table, calendar, gantt,\nworkload, portfolio and roadmap all run through it, so a task filtered out\nof one is filtered out of all, and one `onChange` saves every view.\n\n```jsx\nimport { createTaskContext, applyView, ViewToolbar, useTaskChanges } from \"../components/blocks/task-model\"; // a Makersclaw app\nimport { createTaskContext, applyView, ViewToolbar, useTaskChanges } from \"@/components/makersclaw/blocks/task-model\"; // a shadcn project\n```\n\nA surface rarely renders it alone: `task-list` (and calendar, gantt,\nworkload, work-views…) are built on it. A view built on it `requires`\ntask-model and imports @dnd-kit only through it. `model.js` and\n`dates.js` are pure (no React): they run on the server too.\n\n## The data\n\n### A task\n\n```js\n{\n id: \"t1\", // string (numbers are stringified)\n title: \"Write the launch email\",\n status: \"todo\", // a status id\n assignees: [\"maya\"], // person ids; [] when unassigned\n start: \"2026-09-22\", // optional; a day, or \"2026-09-22T09:30\"\n due: \"2026-09-24T17:00\", // optional; same forms\n estimate: 4, // optional, hours\n progress: 40, // optional, 0–100 (else from status / subtasks)\n priority: \"high\", // optional: urgent · high · medium · low\n parentId: \"t0\", // optional: a subtask of t0\n dependsOn: [\"t7\"], // optional: t7 must finish first\n location: { lat, lng, label }, // optional, for a map view\n tags: [\"copy\"], // []\n project: \"site\", // optional, a project id\n order: 2048, // optional: the hand-made order (see placeChange)\n …extra // any other key is kept and passed through\n}\n```\n\n`normalizeTask(t)` makes the lists lists and the id a string (it also\ntakes `name` for `title` and a single `assignee`); every helper does this\nfor you. Dates are wall-calendar strings, never Date objects: they compare\nin order, survive JSON and a `date` column, and mean the same day in every\ntimezone. A time is local wall time, no zone.\n\n### The context\n\n```js\nconst ctx = useMemo(() => createTaskContext({\n statuses: [{ id: \"todo\", label: \"To do\", tone: \"neutral\" }, …, { id: \"done\", label: \"Done\", tone: \"success\", done: true }],\n people: [{ id: \"maya\", name: \"Maya Chen\", src?, capacity?: 6 }], // capacity: hours a day (default 8)\n projects: [{ id: \"site\", name: \"Website relaunch\", hue?: 1 }], // hue 1–5 = chart-1…5\n fields: [{ key: \"channel\", label: \"Channel\", kind: \"select\", options: [\"Email\", \"Social\"] }],\n today: \"2026-09-23\", // fix it in a demo; default the browser's today\n weekStartsOn: 1, // 0 Sunday … 6; default Monday\n weekend: [0, 6], holidays: [\"2026-12-25\"], // for working-day math\n}), [statuses, people, projects]);\n```\n\nA status's `tone` is neutral · info · warning · success · danger · primary\n(or `hue` 1–5 for a category-like status). `done: true` marks the done\nstatuses (several may be). Without `statuses` you get `DEFAULT_STATUSES`\n(To do, In progress, In review, Done). Custom field kinds: text · number ·\nselect · date · checkbox. Every helper takes `ctx` (or the plain options\nobject, built on the fly — build it once instead).\n\nWhat `ctx` holds: `statuses`, `people`, `projects`, `fields` (built-in +\ncustom, see `TASK_FIELDS`), `statusById` / `personById` / `projectById`\n(Maps; a status also has its `index`), `doneIds` (Set), `today`,\n`weekStartsOn`, `weekend`, `holidays`.\n\n### A view\n\nWhat a view remembers, and what work-views saves per tab:\n\n```js\nDEFAULT_VIEW = { search: \"\", filters: {}, groupBy: \"status\", sort: [], fields: [\"assignees\", \"due\", \"priority\"], hideDone: false }\n```\n\n- `filters` — `{ status: [\"todo\", \"doing\"], assignees: [\"maya\", NONE], due: [\"overdue\", \"today\"] }`:\n any value within a field, every field. `NONE` (`\"__none\"`) is\n \"Unassigned / No date / No project…\". Date fields filter by due bucket.\n- `groupBy` — any field key, or `\"none\"`.\n- `sort` — `[{ key, dir: \"asc\" | \"desc\" }]`; `[]` is the manual order\n (`order`). Keys: any field, or `\"manual\"`.\n- `fields` — which fields a row shows (the list's \"Fields\" menu).\n\n### A change — how a view reports what the founder did\n\nA view never edits tasks. It builds a Change and commits it:\n\n```js\n{ type: \"create\", task } or { type: \"create\", tasks: [...] }\n{ type: \"update\", id, patch } or { type: \"update\", ids: [...], patch }\n{ type: \"delete\", id } or { type: \"delete\", ids: [...] }\n{ type: \"batch\", changes: [...] } // several at once (a renumber, an undo)\n```\n\nThe page answers every change through ONE async callback,\n`onChange(change) → { ok: true } | { ok: false, error }` (a throw counts as\nfailed; `{ ok: true, id }` may return a create's database id). In a\nMakersclaw app it is a loop over `changeWrites(change)`:\n\n```js\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"due\", \"priority\", \"parentId\", \"order\"]; // your table's\n\nasync function onChange(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r = w.op === \"insert\" ? await insertRow({ source: \"public.tasks\", values: w.values })\n : w.op === \"update\" ? await updateRow({ source: \"public.tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"public.tasks\", id: w.id });\n if (r.ok === false) return r;\n }\n return { ok: true };\n}\n```\n\n## Props\n\ntask-model has no one component; these are its exports, grouped by what\na view builder needs. Every one is importable from the block's index.\n\n### Views: search, filter, group, sort\n\n- `applyView(tasks, view, ctx)` → `{ tasks, groups, total }` — the whole\n pipeline: normalise, hide done (`hideDone`), search, filter, sort, group.\n Use this and you match every other view.\n- `searchTasks(tasks, query, ctx)` — every word must appear in the\n title, tags, people's names, project, status label, description or a\n custom text field. `searchText(task, ctx)` is that haystack.\n- `filterTasks(tasks, filters, ctx)` · `activeFilterCount(view)`.\n- `sortTasks(tasks, sort, ctx)` — stable, blanks last either way; status\n sorts in the statuses' order, priority urgent → low, people by name,\n dates by day then time. `sortOptions(ctx)` for a menu.\n- `groupTasks(tasks, by, ctx, { showEmpty? })` →\n `[{ key, label, tone?, hue?, option, tasks }]` in the field's order,\n the \"none\" group last. A task with two assignees (or tags) is in both\n groups. Empty groups are kept for status and priority (somewhere to\n drop) or with `showEmpty: true`. `groupOptions(ctx)` for a menu.\n- `valuesOf(task, key, ctx)` — the group keys a task is filed under\n (`[\"maya\", \"leo\"]`, `[\"overdue\"]`, `[NONE]`).\n- `optionsOf(key, ctx, tasks?)` → `[{ id, label, tone?, hue?, person?, project?, none? }]`\n — a field's choices in order; `labelOf(key, value, ctx)` — one's words.\n- `movePatch(task, by, fromKey, toKey, ctx)` — the patch that moves a task\n between groups of `by`: a new status; one assignee swapped for another\n (the rest stay); a due date for a due bucket (a start moves with it,\n the length kept); `null` when the target is not a place to drop\n (Overdue). Board columns, list groups, workload rows all use it.\n- `fieldOf(key, ctx)` · `TASK_FIELDS` · `PRIORITIES` · `NONE` ·\n `DEFAULT_STATUSES` · `DEFAULT_VIEW`.\n- `isDone(task, ctx)` · `doneStatusId(ctx)` · `openStatusId(ctx)` ·\n `toggleDoneChange(task, ctx)` (the completion checkbox's change).\n\n### Manual order\n\n- `orderOf(task)` — its `order`, or null.\n- `rankBetween(a, b)` — a number between two orders (either may be null).\n- `placeChange(list, task, index, patch?)` — the Change that puts `task` at\n `index` of `list` (the target group's tasks as shown, without the task),\n with `patch` (its new group) merged in: a midpoint `order` when there is\n room, else one `batch` that renumbers the list. Drag-to-reorder is this\n one call.\n- `draftTask(title, ctx, patch?, { after? })` — a new task for a quick-add:\n a client id (`newTaskId()`), the first open status, an order after\n `after`, the group's patch.\n\n### Subtasks\n\n- `flattenTree(tasks, { collapsed? })` → `[{ task, depth, childCount, hasChildren, collapsed }]`\n in reading order; a task whose parent is not in `tasks` is a root.\n- `buildTree(tasks)` → `[{ task, depth, children }]` · `childrenMap(tasks)`.\n- `descendantIds(tasks, id)` · `ancestorIds(tasks, id)`.\n- `subtaskCounts(tasks, ctx)` → Map id → `{ total, done }` (a row's \"2/5\").\n- `rollup(tasks, id, ctx)` → `{ count, done, estimate, progress }`;\n `progressOf(task, tasks, ctx)` → 0–100 (its own, else 100 if done, else\n from its subtasks weighted by estimate).\n- `deleteChange(tasks, ids)` — a delete that takes the subtasks too.\n\n### Dates (dates.js — the calendar, gantt and workload math)\n\nDays are `\"YYYY-MM-DD\"`, optionally `\"…THH:mm\"`.\n\n- Reading: `isDay(v)` · `dayOf(v)` (a day, from a datetime or a Date) ·\n `minutesOf(v)` (minutes after midnight, null for all-day) ·\n `withTime(day, minutes)` · `todayDay()` · `dayNumber(day)` /\n `fromDayNumber(n)` (integer days, for pixel math: `x = (dayNumber(d) - dayNumber(start)) * dayWidth`).\n- Arithmetic: `addDays` · `diffDays(a, b)` (b − a) · `addMonths` (clamps\n to the month's end) · `weekdayOf` (0 Sunday) · `startOfWeek(day, ws)` ·\n `endOfWeek` · `weekDays(day, ws)` (7 days) · `startOfMonth` ·\n `endOfMonth` · `daysBetween(a, b)` (inclusive list).\n- Grids: `monthGrid(\"2026-09\", { weekStartsOn, fixedWeeks })` → weeks of\n 7 days (mute the days whose `slice(0, 7)` is not the month) ·\n `timeScale(from, to, unit, { weekStartsOn, locale })` → columns\n `{ key, start, end, label }` for unit day · week · month · quarter (a\n gantt header, workload columns, a roadmap's quarters) ·\n `rangeOfTasks(tasks, { pad, fallback })` → the window all tasks fit in.\n- Working days: `WEEKEND` · `isWeekend` · `isWorkingDay(day, { weekend, holidays })` ·\n `workingDaysBetween(a, b, opts)` · `addWorkingDays(day, n, opts)`.\n- Spans: `spanOf(task)` → `{ start, end }` (one date → a one-day span;\n none → null = unscheduled) · `spansOverlap(a, b)` ·\n `tasksInRange(tasks, from, to)` · `tasksOnDay(tasks, day)`.\n- Patches a drag saves: `shiftDates(task, n)` (move by n days, keeping\n length and times) · `spanPatch(task, start, end)` (a resize).\n- Words: `formatDay(day, today)` (\"Today\", \"Tomorrow\", \"Yesterday\", \"Fri\",\n \"Oct 30\", \"Jan 3, 2027\") · `formatTime(v)` (\"2:30 PM\") ·\n `formatDate(day, intlOptions)`.\n- Due buckets: `dueBucket(due, today, { done, weekStartsOn })` → overdue ·\n today · tomorrow · this-week · next-week · later · past (a done task's\n old date) · none; `DUE_BUCKETS` (their words and tones) ·\n `dayForBucket(bucket, today)` (the day a drop into a bucket sets).\n\nThe kit's own `addDays` / `startOfMonth` / `formatDate` exist too, with\nthe same string dates; import one or the other in a file, not both.\n\n### Capacity\n\n- `workload(tasks, ctx, { from, to, unit: \"day\" | \"week\", defaultCapacity: 8, includeDone })`\n → `{ rows: [{ person, total, capacity, buckets: [{ key, start, end, hours, capacity, over, tasks: [{ task, hours }] }] }], buckets, unassigned, unscheduled }`.\n A task's estimate is spread evenly over the working days of its span and\n split between its assignees; a bucket's capacity is the person's hours a\n day × its working days. `over` is load past capacity.\n- `hoursPerDay(task, ctx)` · `sumOf(tasks, key = \"estimate\")`.\n\n### Dependencies\n\n- `dependencyEdges(tasks)` → `[{ from, to }]` (`to` waits on `from`).\n- `blockersOf(task, tasks, ctx)` — what it waits on that is not done.\n- `wouldCycle(tasks, fromId, toId)` — refuse a link that loops.\n- `dateConflicts(tasks)` — edges whose `to` starts before `from` ends.\n- `criticalPath(tasks)` → Set of ids on the longest dependent chain.\n\n### Changes\n\n- `useTaskChanges(tasks, onChange)` → `{ tasks, commit, error, clearError, saving }`\n — the hook every view uses. `commit(change, { label?, undo? })` shows\n the change at once, calls `onChange`, and on `{ ok: false }` takes it\n back and sets `error` (\"“Write brief” was not saved: … It is back as it\n was.\") — show it in a `Notice`. `undo: \"Deleted “Write brief”\"` also\n shows a toast with Undo (it commits the inverse; deleted tasks come back\n with their ids). It returns the page's answer. Example rows\n (`useFixture`, `_sample`) change on screen and are never sent. The\n change stays on screen until the page's `tasks` move on, so nothing\n flickers back between the save and the re-read.\n- `applyChange(tasks, change)` · `invertChange(tasksBefore, change)` ·\n `changedIds(change)`.\n- `changeWrites(change, { columns?, keepNewIds? })` — the change as plain\n `{ op: \"insert\", values } | { op: \"update\", id, patch } | { op: \"delete\", id }`\n rows. `columns` keeps only your table's keys (a new task carries empty\n `tags` and `dependsOn` lists your table may not have); `keepNewIds:\n false` drops a created task's client id (`new-…`, see `isNewId`) so the\n table makes its own. Keep it (the default) when your id column is text\n and the client id may stand.\n\n### Adapters for the existing blocks\n\nDrive `board` and `data-table` from tasks without touching them:\n\n```jsx\nconst b = toBoard(visibleTasks, { groupBy: \"status\", ctx }); // or \"assignees\", \"priority\", \"due\"…\n<Board rows={b.rows} groupBy={b.groupBy} columns={b.columns} otherLabel={b.otherLabel}\n onMove={(row, to, from) => commit(b.moveChange(row, to, from), { label: `“${row.title}”` })}\n titleOf={(t) => t.title}\n renderDetails={(t) => <><DueLabel value={t.due} ctx={ctx} /><AssigneeStack ids={t.assignees} ctx={ctx} /></>} />\n\n<DataTable rows={toTableRows(visibleTasks, ctx)}\n columns={taskColumns(ctx, { keys: [\"title\", \"status\", \"assignees\", \"due\", \"priority\"], onChange: commit })}\n facets={taskFacets(ctx)} noun=\"task\" />\n```\n\n- `toBoard(tasks, { groupBy, ctx })` → `{ rows, groupBy: \"__group\", columns, otherLabel, moveChange(row, to, from) }`\n — one card per task, under its first value; `moveChange` is\n `movePatch` as an update (null when the column refuses it). Overdue is\n not a column when grouped by due date.\n- `toTableRows(tasks, ctx)` → the tasks plus `statusLabel`, `statusIndex`,\n `assigneeNames`, `projectName`, `priorityLabel`, `priorityRank`,\n `dueDay`, `dueBucket`, `parentTitle`, `subtasks` (\"2/5\"), `done`.\n- `taskColumns(ctx, { keys?, onChange? })` — DataTable columns (title,\n status, assignees, due, start, priority, project, estimate, progress,\n tags), sortable, drawn with the parts; with `onChange` the status,\n people, due and priority cells are pickers that send an update Change.\n- `taskFacets(ctx, { keys? })` — facets for status, assignee, priority,\n project.\n\n### Parts every view shares\n\nEach is a label; with `onChange` it becomes a picker (a kit Menu or\nPopover) and hands the picked value back — you wrap it in a Change.\n\n- `<StatusPill status ctx onChange?(id) size?=\"sm\"|\"md\" hideLabel? />` — dot + words.\n- `<CompleteToggle done onToggle label size?=\"md\"|\"sm\" />` — the round\n completion check (not the square selection Checkbox).\n- `<AssigneeStack ids ctx onChange?(ids) max?=3 size?=\"xs\" />` — faces,\n or a dashed \"assign\" circle; the picker ticks people (stays open).\n- `<PersonLabel id ctx />` — face + name (\"Unassigned\" for NONE).\n- `<DueLabel value done? ctx onChange?(day|null) quiet?=true placeholder? />`\n — \"Today\" / \"Fri\" / \"Oct 30\", danger when overdue, warning today; the\n picker is quick picks (Today, Tomorrow, Next week) + a calendar + Remove.\n `quiet` hides an empty one's words until its row (`group/row`) is hovered.\n- `<PriorityFlag value onChange?(id|null) showLabel? />` — a flag in the\n priority's tone.\n- `<TagList tags max?=2 />` · `<SubtaskCount done total />` ·\n `<ProgressLine value />` (0–100).\n- `<TaskChip task ctx onOpen? showTime? showPeople? dragging? lifted? dragProps? />`\n — a task on one compact line (status dot, time, title, first face, the\n project's hue on its leading edge): a calendar day's item, a gantt\n label, a workload cell.\n- `<GroupLabel group by ctx />` — a group heading's face for any field.\n- `TONE_TEXT` · `HUE_EDGE` · `HUE_BG` — the token classes those use\n (tone → `text-…`, hue 1–5 → `border-s-chart-N` / `bg-chart-N`).\n\nRows that host pickers should carry the `group/row` class: the quiet\nparts (an empty due date, an empty priority, a drag handle) appear when\nthe row is hovered or focused, and always on touch.\n\n### ViewToolbar\n\n```jsx\n<ViewToolbar view={view} onViewChange={setView} ctx={ctx} tasks={allTasks}\n count={shown.tasks.length} extra={<Button variant=\"primary\" size=\"sm\" icon=\"plus\">Add task</Button>} />\n```\n\nSearch (Escape clears), Filter (a popover of every filterable field with\ncounts, and \"Hide done tasks\"), Group by, Sort (field + direction), Fields\n(which fields rows show), your `extra` at the end; active filters as\nchips underneath, each with ×, and \"Clear filters\". On a phone the search\ntakes the first line and the controls are icons.\n\n- `view` · `onViewChange(nextView)` — controlled; a view object as above.\n- `ctx` · `tasks` (the counts in the filter menu).\n- `controls` — default `[\"search\", \"filter\", \"group\", \"sort\", \"fields\"]`;\n a calendar might pass `[\"search\", \"filter\"]`.\n- `filterFields` · `groupFields` · `showableFields` — limit the choices.\n- `count` — \"12 tasks\" beside the controls (from 768px).\n- `searchPlaceholder` (default \"Search tasks\") · `extra` · `className`.\n\n### Drag primitives (@dnd-kit, never imported directly)\n\nOne interaction model everywhere: a mouse drags after 5px, a finger after\na short press (so lists still scroll), the keyboard picks up with Space\nor Enter, moves with the arrows, drops with Space, cancels with Escape;\nscreen readers hear what was picked up and where it is.\n\n```jsx\n<TaskDragProvider\n onDragEnd={({ active, over }) => { /* active.data / over.data are what you attached */ }}\n overlay={(active) => <div className={LIFTED}><TaskChip task={active.data.task} ctx={ctx} /></div>}\n collision=\"list\">\n <SortableGroup ids={ids}>…rows using useSortableTask…</SortableGroup>\n …cells using useDropTarget, chips using useDraggableTask…\n</TaskDragProvider>\n```\n\n- `<TaskDragProvider onDragStart? onDragOver? onDragEnd? onDragCancel? overlay? collision? describe? place? disabled? />`\n — callbacks get `{ active: { id, data }, over: { id, data } | null, delta }`.\n `overlay(active)` draws the lifted copy (tilted 1°, 150ms drop; flat\n and instant under reduced motion). `collision`: `\"list\"` (default: the\n item under the pointer, else the group under it, else — keyboard — the\n closest) · `\"closest\"` · `\"pointer\"` (grids and calendars) · `\"rect\"` ·\n a function. `describe(active)` / `place(over)` word the announcements\n (default `data.task.title`, `data.label`).\n- `<SortableGroup ids layout?=\"vertical\"|\"horizontal\"|\"grid\">` — items that\n reorder among themselves.\n- `useSortableTask(id, data, { disabled })` → `{ setNodeRef, setHandleRef, style, handle, isDragging, isOver }`\n — `setNodeRef` + `style` on the item, `handle` spread on its grip.\n- `useDraggableTask(id, data)` → `{ setNodeRef, setHandleRef, handle, style, transform, isDragging }`\n — a free item dropped on targets (a calendar chip, a gantt bar; use\n `delta` in onDragEnd for a resize or a pixel shift).\n- `useDropTarget(id, data)` → `{ setNodeRef, isOver, active }` — a group,\n a day cell, a person's week.\n- `<DragHandle handle handleRef? label alwaysVisible? />` — the grip, shown\n on row hover (always on touch). `<DropLine />` — a landing line.\n- `LIFTED` — the lifted-card classes · `useReducedMotion()` · `arrayMove`.\n\nHow a drop becomes a save: read `over.data` (a group key, a day, a\nperson), build the patch (`movePatch`, `shiftDates`, `spanPatch`, or\n`placeChange` for an order), `commit` it.\n\n## Example\n\nA minimal view on the model — search, filter and group from the toolbar,\ntasks as chips, drag a chip to another status:\n\n```jsx\n\"use client\";\nimport { useMemo, useState } from \"react\";\nimport { Notice } from \"./ui\";\nimport {\n DEFAULT_VIEW, TaskChip, TaskDragProvider, ViewToolbar, applyView, createTaskContext,\n movePatch, useDraggableTask, useDropTarget, useTaskChanges, LIFTED,\n} from \"./blocks/task-model\";\n\nexport default function StatusLanes({ tasks, statuses, people, onChange }) {\n const ctx = useMemo(() => createTaskContext({ statuses, people }), [statuses, people]);\n const { tasks: live, commit, error, clearError } = useTaskChanges(tasks, onChange);\n const [view, setView] = useState({ ...DEFAULT_VIEW, groupBy: \"status\" });\n const { groups } = applyView(live, view, ctx);\n return (\n <div className=\"grid gap-3\">\n <ViewToolbar view={view} onViewChange={setView} ctx={ctx} tasks={live} controls={[\"search\", \"filter\"]} />\n {error ? <Notice tone=\"danger\" onDismiss={clearError}>{error}</Notice> : null}\n <TaskDragProvider collision=\"pointer\"\n overlay={(a) => <div className={LIFTED}><TaskChip task={a.data.task} ctx={ctx} /></div>}\n onDragEnd={({ active, over }) => {\n if (!over) return;\n const patch = movePatch(active.data.task, \"status\", active.data.group, over.data.group, ctx);\n if (patch && over.data.group !== active.data.group) commit({ type: \"update\", id: active.data.task.id, patch });\n }}>\n {groups.map((g) => <Lane key={g.key} group={g} ctx={ctx} />)}\n </TaskDragProvider>\n </div>\n );\n}\n\nfunction Lane({ group, ctx }) {\n const drop = useDropTarget(`lane-${group.key}`, { group: group.key, label: group.label });\n return (\n <section ref={drop.setNodeRef} className={drop.isOver ? \"rounded-lg bg-accent p-2\" : \"rounded-lg p-2\"}>\n <h3 className=\"text-sm font-semibold\">{group.label}</h3>\n <ul className=\"mt-2 grid gap-1\">{group.tasks.map((t) => <Chip key={t.id} task={t} group={group.key} ctx={ctx} />)}</ul>\n </section>\n );\n}\n\nfunction Chip({ task, group, ctx }) {\n const d = useDraggableTask(task.id, { task, group });\n return <li ref={d.setNodeRef}><TaskChip task={task} ctx={ctx} dragging={d.isDragging} dragProps={d.handle} /></li>;\n}\n```\n\n## What it will not do\n\n- Draw a whole view: that is `task-list`, `board` (through `toBoard`),\n `data-table` (through `toTableRows`), and the calendar / gantt /\n workload blocks built on this one.\n- Fetch or save: tasks come in as props, changes go out through the\n page's `onChange`.\n- Recurring tasks, time zones, or time tracking: a date is a wall-calendar\n day (and time); a repeat is the page's to create.\n- Sync between people live: the page re-reads.\n\nIts files are fixed: `npm run check` refuses a change, and `makersclaw\nadd task-model` restores them. A view it cannot be is your own component\nbuilt from these exports.\n\n## Credits\n\nThe drag primitives wrap @dnd-kit (MIT, https://dndkit.com): core 6.3.1,\nsortable 10.0.0, utilities 3.2.2.\n"
62
+ "content": "# Task model\n\nThe shared model every view of work reads: one task shape, one context\n(statuses, people, projects, custom fields, today), the arithmetic every\nview repeats (search, filter, group, sort, subtasks, dates, capacity,\ndependencies), the Change a view sends when the founder moves something,\nthe small parts every view draws the same way, the toolbar above them, and\nthe drag primitives on @dnd-kit. List, board, table, calendar, gantt,\nworkload, portfolio and roadmap all run through it, so a task filtered out\nof one is filtered out of all, and one `onChange` saves every view.\n\n```jsx\nimport { createTaskContext, applyView, ViewToolbar, useTaskChanges } from \"../components/blocks/task-model\"; // a Makersclaw app\nimport { createTaskContext, applyView, ViewToolbar, useTaskChanges } from \"@/components/makersclaw/blocks/task-model\"; // a shadcn project\n```\n\nA surface rarely renders it alone: `task-list` (and calendar, gantt,\nworkload, work-views…) are built on it. A view built on it `requires`\ntask-model and imports @dnd-kit only through it. `model.js` and\n`dates.js` are pure (no React): they run on the server too.\n\n## The data\n\n### A task\n\n```js\n{\n id: \"t1\", // string (numbers are stringified)\n title: \"Write the launch email\",\n status: \"todo\", // a status id\n assignees: [\"maya\"], // person ids; [] when unassigned\n start: \"2026-09-22\", // optional; a day, or \"2026-09-22T09:30\"\n due: \"2026-09-24T17:00\", // optional; same forms\n estimate: 4, // optional, hours\n progress: 40, // optional, 0–100 (else from status / subtasks)\n priority: \"high\", // optional: urgent · high · medium · low\n parentId: \"t0\", // optional: a subtask of t0\n dependsOn: [\"t7\"], // optional: t7 must finish first\n location: { lat, lng, label }, // optional, for a map view\n tags: [\"copy\"], // []\n project: \"site\", // optional, a project id\n order: 2048, // optional: the hand-made order (see placeChange)\n …extra // any other key is kept and passed through\n}\n```\n\n`normalizeTask(t)` makes the lists lists and the id a string (it also\ntakes `name` for `title` and a single `assignee`); every helper does this\nfor you. Dates are wall-calendar strings, never Date objects: they compare\nin order, survive JSON and a `date` column, and mean the same day in every\ntimezone. A time is local wall time, no zone.\n\n### The context\n\n```js\nconst ctx = useMemo(() => createTaskContext({\n statuses: [{ id: \"todo\", label: \"To do\", tone: \"neutral\" }, …, { id: \"done\", label: \"Done\", tone: \"success\", done: true }],\n people: [{ id: \"maya\", name: \"Maya Chen\", src?, capacity?: 6 }], // capacity: hours a day (default 8)\n projects: [{ id: \"site\", name: \"Website relaunch\", hue?: 1 }], // hue 1–5 = chart-1…5\n fields: [{ key: \"channel\", label: \"Channel\", kind: \"select\", options: [\"Email\", \"Social\"] }],\n today: \"2026-09-23\", // fix it in a demo; default the browser's today\n weekStartsOn: 1, // 0 Sunday … 6; default Monday\n weekend: [0, 6], holidays: [\"2026-12-25\"], // for working-day math\n}), [statuses, people, projects]);\n```\n\nA status's `tone` is neutral · info · warning · success · danger · primary\n(or `hue` 1–5 for a category-like status). `done: true` marks the done\nstatuses (several may be). Without `statuses` you get `DEFAULT_STATUSES`\n(To do, In progress, In review, Done). Custom field kinds: text · number ·\nselect · date · checkbox. Every helper takes `ctx` (or the plain options\nobject, built on the fly — build it once instead).\n\nWhat `ctx` holds: `statuses`, `people`, `projects`, `fields` (built-in +\ncustom, see `TASK_FIELDS`), `statusById` / `personById` / `projectById`\n(Maps; a status also has its `index`), `doneIds` (Set), `today`,\n`weekStartsOn`, `weekend`, `holidays`.\n\n### A view\n\nWhat a view remembers, and what work-views saves per tab:\n\n```js\nDEFAULT_VIEW = { search: \"\", filters: {}, groupBy: \"status\", sort: [], fields: [\"assignees\", \"due\", \"priority\"], hideDone: false }\n```\n\n- `filters` — `{ status: [\"todo\", \"doing\"], assignees: [\"maya\", NONE], due: [\"overdue\", \"today\"] }`:\n any value within a field, every field. `NONE` (`\"__none\"`) is\n \"Unassigned / No date / No project…\". Date fields filter by due bucket.\n- `groupBy` — any field key, or `\"none\"`.\n- `sort` — `[{ key, dir: \"asc\" | \"desc\" }]`; `[]` is the manual order\n (`order`). Keys: any field, or `\"manual\"`.\n- `fields` — which fields a row shows (the list's \"Fields\" menu).\n\n### A change — how a view reports what the founder did\n\nA view never edits tasks. It builds a Change and commits it:\n\n```js\n{ type: \"create\", task } or { type: \"create\", tasks: [...] }\n{ type: \"update\", id, patch } or { type: \"update\", ids: [...], patch }\n{ type: \"delete\", id } or { type: \"delete\", ids: [...] }\n{ type: \"batch\", changes: [...] } // several at once (a renumber, an undo)\n```\n\nThe page answers every change through ONE async callback,\n`onChange(change) → { ok: true } | { ok: false, error }` (a throw counts as\nfailed; `{ ok: true, id }` may return a create's database id). In a\nMakersclaw app it is a loop over `changeWrites(change)`:\n\n```js\nconst COLUMNS = [\"id\", \"title\", \"status\", \"assignees\", \"due\", \"priority\", \"parentId\", \"order\"]; // your table's\n\nasync function onChange(change) {\n for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {\n const r = w.op === \"insert\" ? await insertRow({ source: \"tasks\", values: w.values })\n : w.op === \"update\" ? await updateRow({ source: \"tasks\", id: w.id, patch: w.patch })\n : await deleteRow({ source: \"tasks\", id: w.id });\n if (r.ok === false) return r;\n }\n return { ok: true };\n}\n```\n\n## Props\n\ntask-model has no one component; these are its exports, grouped by what\na view builder needs. Every one is importable from the block's index.\n\n### Views: search, filter, group, sort\n\n- `applyView(tasks, view, ctx)` → `{ tasks, groups, total }` — the whole\n pipeline: normalise, hide done (`hideDone`), search, filter, sort, group.\n Use this and you match every other view.\n- `searchTasks(tasks, query, ctx)` — every word must appear in the\n title, tags, people's names, project, status label, description or a\n custom text field. `searchText(task, ctx)` is that haystack.\n- `filterTasks(tasks, filters, ctx)` · `activeFilterCount(view)`.\n- `sortTasks(tasks, sort, ctx)` — stable, blanks last either way; status\n sorts in the statuses' order, priority urgent → low, people by name,\n dates by day then time. `sortOptions(ctx)` for a menu.\n- `groupTasks(tasks, by, ctx, { showEmpty? })` →\n `[{ key, label, tone?, hue?, option, tasks }]` in the field's order,\n the \"none\" group last. A task with two assignees (or tags) is in both\n groups. Empty groups are kept for status and priority (somewhere to\n drop) or with `showEmpty: true`. `groupOptions(ctx)` for a menu.\n- `valuesOf(task, key, ctx)` — the group keys a task is filed under\n (`[\"maya\", \"leo\"]`, `[\"overdue\"]`, `[NONE]`).\n- `optionsOf(key, ctx, tasks?)` → `[{ id, label, tone?, hue?, person?, project?, none? }]`\n — a field's choices in order; `labelOf(key, value, ctx)` — one's words.\n- `movePatch(task, by, fromKey, toKey, ctx)` — the patch that moves a task\n between groups of `by`: a new status; one assignee swapped for another\n (the rest stay); a due date for a due bucket (a start moves with it,\n the length kept); `null` when the target is not a place to drop\n (Overdue). Board columns, list groups, workload rows all use it.\n- `fieldOf(key, ctx)` · `TASK_FIELDS` · `PRIORITIES` · `NONE` ·\n `DEFAULT_STATUSES` · `DEFAULT_VIEW`.\n- `isDone(task, ctx)` · `doneStatusId(ctx)` · `openStatusId(ctx)` ·\n `toggleDoneChange(task, ctx)` (the completion checkbox's change).\n\n### Manual order\n\n- `orderOf(task)` — its `order`, or null.\n- `rankBetween(a, b)` — a number between two orders (either may be null).\n- `placeChange(list, task, index, patch?)` — the Change that puts `task` at\n `index` of `list` (the target group's tasks as shown, without the task),\n with `patch` (its new group) merged in: a midpoint `order` when there is\n room, else one `batch` that renumbers the list. Drag-to-reorder is this\n one call.\n- `draftTask(title, ctx, patch?, { after? })` — a new task for a quick-add:\n a client id (`newTaskId()`), the first open status, an order after\n `after`, the group's patch.\n\n### Subtasks\n\n- `flattenTree(tasks, { collapsed? })` → `[{ task, depth, childCount, hasChildren, collapsed }]`\n in reading order; a task whose parent is not in `tasks` is a root.\n- `buildTree(tasks)` → `[{ task, depth, children }]` · `childrenMap(tasks)`.\n- `descendantIds(tasks, id)` · `ancestorIds(tasks, id)`.\n- `subtaskCounts(tasks, ctx)` → Map id → `{ total, done }` (a row's \"2/5\").\n- `rollup(tasks, id, ctx)` → `{ count, done, estimate, progress }`;\n `progressOf(task, tasks, ctx)` → 0–100 (its own, else 100 if done, else\n from its subtasks weighted by estimate).\n- `deleteChange(tasks, ids)` — a delete that takes the subtasks too.\n\n### Dates (dates.js — the calendar, gantt and workload math)\n\nDays are `\"YYYY-MM-DD\"`, optionally `\"…THH:mm\"`.\n\n- Reading: `isDay(v)` · `dayOf(v)` (a day, from a datetime or a Date) ·\n `minutesOf(v)` (minutes after midnight, null for all-day) ·\n `withTime(day, minutes)` · `todayDay()` · `dayNumber(day)` /\n `fromDayNumber(n)` (integer days, for pixel math: `x = (dayNumber(d) - dayNumber(start)) * dayWidth`).\n- Arithmetic: `addDays` · `diffDays(a, b)` (b − a) · `addMonths` (clamps\n to the month's end) · `weekdayOf` (0 Sunday) · `startOfWeek(day, ws)` ·\n `endOfWeek` · `weekDays(day, ws)` (7 days) · `startOfMonth` ·\n `endOfMonth` · `daysBetween(a, b)` (inclusive list).\n- Grids: `monthGrid(\"2026-09\", { weekStartsOn, fixedWeeks })` → weeks of\n 7 days (mute the days whose `slice(0, 7)` is not the month) ·\n `timeScale(from, to, unit, { weekStartsOn, locale })` → columns\n `{ key, start, end, label }` for unit day · week · month · quarter (a\n gantt header, workload columns, a roadmap's quarters) ·\n `rangeOfTasks(tasks, { pad, fallback })` → the window all tasks fit in.\n- Working days: `WEEKEND` · `isWeekend` · `isWorkingDay(day, { weekend, holidays })` ·\n `workingDaysBetween(a, b, opts)` · `addWorkingDays(day, n, opts)`.\n- Spans: `spanOf(task)` → `{ start, end }` (one date → a one-day span;\n none → null = unscheduled) · `spansOverlap(a, b)` ·\n `tasksInRange(tasks, from, to)` · `tasksOnDay(tasks, day)`.\n- Patches a drag saves: `shiftDates(task, n)` (move by n days, keeping\n length and times) · `spanPatch(task, start, end)` (a resize).\n- Words: `formatDay(day, today)` (\"Today\", \"Tomorrow\", \"Yesterday\", \"Fri\",\n \"Oct 30\", \"Jan 3, 2027\") · `formatTime(v)` (\"2:30 PM\") ·\n `formatDate(day, intlOptions)`.\n- Due buckets: `dueBucket(due, today, { done, weekStartsOn })` → overdue ·\n today · tomorrow · this-week · next-week · later · past (a done task's\n old date) · none; `DUE_BUCKETS` (their words and tones) ·\n `dayForBucket(bucket, today)` (the day a drop into a bucket sets).\n\nThe kit's own `addDays` / `startOfMonth` / `formatDate` exist too, with\nthe same string dates; import one or the other in a file, not both.\n\n### Capacity\n\n- `workload(tasks, ctx, { from, to, unit: \"day\" | \"week\", defaultCapacity: 8, includeDone })`\n → `{ rows: [{ person, total, capacity, buckets: [{ key, start, end, hours, capacity, over, tasks: [{ task, hours }] }] }], buckets, unassigned, unscheduled }`.\n A task's estimate is spread evenly over the working days of its span and\n split between its assignees; a bucket's capacity is the person's hours a\n day × its working days. `over` is load past capacity.\n- `hoursPerDay(task, ctx)` · `sumOf(tasks, key = \"estimate\")`.\n\n### Dependencies\n\n- `dependencyEdges(tasks)` → `[{ from, to }]` (`to` waits on `from`).\n- `blockersOf(task, tasks, ctx)` — what it waits on that is not done.\n- `wouldCycle(tasks, fromId, toId)` — refuse a link that loops.\n- `dateConflicts(tasks)` — edges whose `to` starts before `from` ends.\n- `criticalPath(tasks)` → Set of ids on the longest dependent chain.\n\n### Changes\n\n- `useTaskChanges(tasks, onChange)` → `{ tasks, commit, error, clearError, saving }`\n — the hook every view uses. `commit(change, { label?, undo? })` shows\n the change at once, calls `onChange`, and on `{ ok: false }` takes it\n back and sets `error` (\"“Write brief” was not saved: … It is back as it\n was.\") — show it in a `Notice`. `undo: \"Deleted “Write brief”\"` also\n shows a toast with Undo (it commits the inverse; deleted tasks come back\n with their ids). It returns the page's answer. Example rows\n (`useFixture`, `_sample`) change on screen and are never sent. The\n change stays on screen until the page's `tasks` move on, so nothing\n flickers back between the save and the re-read.\n- `applyChange(tasks, change)` · `invertChange(tasksBefore, change)` ·\n `changedIds(change)`.\n- `changeWrites(change, { columns?, keepNewIds? })` — the change as plain\n `{ op: \"insert\", values } | { op: \"update\", id, patch } | { op: \"delete\", id }`\n rows. `columns` keeps only your table's keys (a new task carries empty\n `tags` and `dependsOn` lists your table may not have); `keepNewIds:\n false` drops a created task's client id (`new-…`, see `isNewId`) so the\n table makes its own. Keep it (the default) when your id column is text\n and the client id may stand.\n\n### Adapters for the existing blocks\n\nDrive `board` and `data-table` from tasks without touching them:\n\n```jsx\nconst b = toBoard(visibleTasks, { groupBy: \"status\", ctx }); // or \"assignees\", \"priority\", \"due\"…\n<Board rows={b.rows} groupBy={b.groupBy} columns={b.columns} otherLabel={b.otherLabel}\n onMove={(row, to, from) => commit(b.moveChange(row, to, from), { label: `“${row.title}”` })}\n titleOf={(t) => t.title}\n renderDetails={(t) => <><DueLabel value={t.due} ctx={ctx} /><AssigneeStack ids={t.assignees} ctx={ctx} /></>} />\n\n<DataTable rows={toTableRows(visibleTasks, ctx)}\n columns={taskColumns(ctx, { keys: [\"title\", \"status\", \"assignees\", \"due\", \"priority\"], onChange: commit })}\n facets={taskFacets(ctx)} noun=\"task\" />\n```\n\n- `toBoard(tasks, { groupBy, ctx })` → `{ rows, groupBy: \"__group\", columns, otherLabel, moveChange(row, to, from) }`\n — one card per task, under its first value; `moveChange` is\n `movePatch` as an update (null when the column refuses it). Overdue is\n not a column when grouped by due date.\n- `toTableRows(tasks, ctx)` → the tasks plus `statusLabel`, `statusIndex`,\n `assigneeNames`, `projectName`, `priorityLabel`, `priorityRank`,\n `dueDay`, `dueBucket`, `parentTitle`, `subtasks` (\"2/5\"), `done`.\n- `taskColumns(ctx, { keys?, onChange? })` — DataTable columns (title,\n status, assignees, due, start, priority, project, estimate, progress,\n tags), sortable, drawn with the parts; with `onChange` the status,\n people, due and priority cells are pickers that send an update Change.\n- `taskFacets(ctx, { keys? })` — facets for status, assignee, priority,\n project.\n\n### Parts every view shares\n\nEach is a label; with `onChange` it becomes a picker (a kit Menu or\nPopover) and hands the picked value back — you wrap it in a Change.\n\n- `<StatusPill status ctx onChange?(id) size?=\"sm\"|\"md\" hideLabel? />` — dot + words.\n- `<CompleteToggle done onToggle label size?=\"md\"|\"sm\" />` — the round\n completion check (not the square selection Checkbox).\n- `<AssigneeStack ids ctx onChange?(ids) max?=3 size?=\"xs\" />` — faces,\n or a dashed \"assign\" circle; the picker ticks people (stays open).\n- `<PersonLabel id ctx />` — face + name (\"Unassigned\" for NONE).\n- `<DueLabel value done? ctx onChange?(day|null) quiet?=true placeholder? />`\n — \"Today\" / \"Fri\" / \"Oct 30\", danger when overdue, warning today; the\n picker is quick picks (Today, Tomorrow, Next week) + a calendar + Remove.\n `quiet` hides an empty one's words until its row (`group/row`) is hovered.\n- `<PriorityFlag value onChange?(id|null) showLabel? />` — a flag in the\n priority's tone.\n- `<TagList tags max?=2 />` · `<SubtaskCount done total />` ·\n `<ProgressLine value />` (0–100).\n- `<TaskChip task ctx onOpen? showTime? showPeople? dragging? lifted? dragProps? />`\n — a task on one compact line (status dot, time, title, first face, the\n project's hue on its leading edge): a calendar day's item, a gantt\n label, a workload cell.\n- `<GroupLabel group by ctx />` — a group heading's face for any field.\n- `TONE_TEXT` · `HUE_EDGE` · `HUE_BG` — the token classes those use\n (tone → `text-…`, hue 1–5 → `border-s-chart-N` / `bg-chart-N`).\n\nRows that host pickers should carry the `group/row` class: the quiet\nparts (an empty due date, an empty priority, a drag handle) appear when\nthe row is hovered or focused, and always on touch.\n\n### ViewToolbar\n\n```jsx\n<ViewToolbar view={view} onViewChange={setView} ctx={ctx} tasks={allTasks}\n count={shown.tasks.length} extra={<Button variant=\"primary\" size=\"sm\" icon=\"plus\">Add task</Button>} />\n```\n\nSearch (Escape clears), Filter (a popover of every filterable field with\ncounts, and \"Hide done tasks\"), Group by, Sort (field + direction), Fields\n(which fields rows show), your `extra` at the end; active filters as\nchips underneath, each with ×, and \"Clear filters\". On a phone the search\ntakes the first line and the controls are icons.\n\n- `view` · `onViewChange(nextView)` — controlled; a view object as above.\n- `ctx` · `tasks` (the counts in the filter menu).\n- `controls` — default `[\"search\", \"filter\", \"group\", \"sort\", \"fields\"]`;\n a calendar might pass `[\"search\", \"filter\"]`.\n- `filterFields` · `groupFields` · `showableFields` — limit the choices.\n- `count` — \"12 tasks\" beside the controls (from 768px).\n- `searchPlaceholder` (default \"Search tasks\") · `extra` · `className`.\n\n### Drag primitives (@dnd-kit, never imported directly)\n\nOne interaction model everywhere: a mouse drags after 5px, a finger after\na short press (so lists still scroll), the keyboard picks up with Space\nor Enter, moves with the arrows, drops with Space, cancels with Escape;\nscreen readers hear what was picked up and where it is.\n\n```jsx\n<TaskDragProvider\n onDragEnd={({ active, over }) => { /* active.data / over.data are what you attached */ }}\n overlay={(active) => <div className={LIFTED}><TaskChip task={active.data.task} ctx={ctx} /></div>}\n collision=\"list\">\n <SortableGroup ids={ids}>…rows using useSortableTask…</SortableGroup>\n …cells using useDropTarget, chips using useDraggableTask…\n</TaskDragProvider>\n```\n\n- `<TaskDragProvider onDragStart? onDragOver? onDragEnd? onDragCancel? overlay? collision? describe? place? disabled? />`\n — callbacks get `{ active: { id, data }, over: { id, data } | null, delta }`.\n `overlay(active)` draws the lifted copy (tilted 1°, 150ms drop; flat\n and instant under reduced motion). `collision`: `\"list\"` (default: the\n item under the pointer, else the group under it, else — keyboard — the\n closest) · `\"closest\"` · `\"pointer\"` (grids and calendars) · `\"rect\"` ·\n a function. `describe(active)` / `place(over)` word the announcements\n (default `data.task.title`, `data.label`).\n- `<SortableGroup ids layout?=\"vertical\"|\"horizontal\"|\"grid\">` — items that\n reorder among themselves.\n- `useSortableTask(id, data, { disabled })` → `{ setNodeRef, setHandleRef, style, handle, isDragging, isOver }`\n — `setNodeRef` + `style` on the item, `handle` spread on its grip.\n- `useDraggableTask(id, data)` → `{ setNodeRef, setHandleRef, handle, style, transform, isDragging }`\n — a free item dropped on targets (a calendar chip, a gantt bar; use\n `delta` in onDragEnd for a resize or a pixel shift).\n- `useDropTarget(id, data)` → `{ setNodeRef, isOver, active }` — a group,\n a day cell, a person's week.\n- `<DragHandle handle handleRef? label alwaysVisible? />` — the grip, shown\n on row hover (always on touch). `<DropLine />` — a landing line.\n- `LIFTED` — the lifted-card classes · `useReducedMotion()` · `arrayMove`.\n\nHow a drop becomes a save: read `over.data` (a group key, a day, a\nperson), build the patch (`movePatch`, `shiftDates`, `spanPatch`, or\n`placeChange` for an order), `commit` it.\n\n## Example\n\nA minimal view on the model — search, filter and group from the toolbar,\ntasks as chips, drag a chip to another status:\n\n```jsx\n\"use client\";\nimport { useMemo, useState } from \"react\";\nimport { Notice } from \"./ui\";\nimport {\n DEFAULT_VIEW, TaskChip, TaskDragProvider, ViewToolbar, applyView, createTaskContext,\n movePatch, useDraggableTask, useDropTarget, useTaskChanges, LIFTED,\n} from \"./blocks/task-model\";\n\nexport default function StatusLanes({ tasks, statuses, people, onChange }) {\n const ctx = useMemo(() => createTaskContext({ statuses, people }), [statuses, people]);\n const { tasks: live, commit, error, clearError } = useTaskChanges(tasks, onChange);\n const [view, setView] = useState({ ...DEFAULT_VIEW, groupBy: \"status\" });\n const { groups } = applyView(live, view, ctx);\n return (\n <div className=\"grid gap-3\">\n <ViewToolbar view={view} onViewChange={setView} ctx={ctx} tasks={live} controls={[\"search\", \"filter\"]} />\n {error ? <Notice tone=\"danger\" onDismiss={clearError}>{error}</Notice> : null}\n <TaskDragProvider collision=\"pointer\"\n overlay={(a) => <div className={LIFTED}><TaskChip task={a.data.task} ctx={ctx} /></div>}\n onDragEnd={({ active, over }) => {\n if (!over) return;\n const patch = movePatch(active.data.task, \"status\", active.data.group, over.data.group, ctx);\n if (patch && over.data.group !== active.data.group) commit({ type: \"update\", id: active.data.task.id, patch });\n }}>\n {groups.map((g) => <Lane key={g.key} group={g} ctx={ctx} />)}\n </TaskDragProvider>\n </div>\n );\n}\n\nfunction Lane({ group, ctx }) {\n const drop = useDropTarget(`lane-${group.key}`, { group: group.key, label: group.label });\n return (\n <section ref={drop.setNodeRef} className={drop.isOver ? \"rounded-lg bg-accent p-2\" : \"rounded-lg p-2\"}>\n <h3 className=\"text-sm font-semibold\">{group.label}</h3>\n <ul className=\"mt-2 grid gap-1\">{group.tasks.map((t) => <Chip key={t.id} task={t} group={group.key} ctx={ctx} />)}</ul>\n </section>\n );\n}\n\nfunction Chip({ task, group, ctx }) {\n const d = useDraggableTask(task.id, { task, group });\n return <li ref={d.setNodeRef}><TaskChip task={task} ctx={ctx} dragging={d.isDragging} dragProps={d.handle} /></li>;\n}\n```\n\n## What it will not do\n\n- Draw a whole view: that is `task-list`, `board` (through `toBoard`),\n `data-table` (through `toTableRows`), and the calendar / gantt /\n workload blocks built on this one.\n- Fetch or save: tasks come in as props, changes go out through the\n page's `onChange`.\n- Recurring tasks, time zones, or time tracking: a date is a wall-calendar\n day (and time); a repeat is the page's to create.\n- Sync between people live: the page re-reads.\n\nIts files are fixed: `npm run check` refuses a change, and `makersclaw\nadd task-model` restores them. A view it cannot be is your own component\nbuilt from these exports.\n\n## Credits\n\nThe drag primitives wrap @dnd-kit (MIT, https://dndkit.com): core 6.3.1,\nsortable 10.0.0, utilities 3.2.2.\n"
63
63
  }
64
64
  ],
65
65
  "categories": [
@@ -91,7 +91,7 @@
91
91
  "docs": "Installed to components/makersclaw/blocks/task-model. Import: import { NONE, PRIORITIES, DEFAULT_STATUSES, TASK_FIELDS, DEFAULT_VIEW, normalizeTask, normalizeTasks, createTaskContext, fieldOf, isDone, doneStatusId, openStatusId, toggleDoneChange, newTaskId, draftTask, valuesOf, optionsOf, labelOf, searchText, searchTasks, filterTasks, orderOf, sortTasks, sortOptions, groupTasks, groupOptions, movePatch, applyView, activeFilterCount, rankBetween, placeChange, childrenMap, buildTree, flattenTree, descendantIds, ancestorIds, subtaskCounts, rollup, progressOf, dependencyEdges, blockersOf, wouldCycle, dateConflicts, criticalPath, hoursPerDay, workload, sumOf, applyChange, invertChange, changeWrites, changedIds, deleteChange, toBoard, toTableRows, taskFacets, StatusPill, CompleteToggle, AssigneeStack, PersonLabel, DueLabel, PriorityFlag, TagList, SubtaskCount, ProgressLine, TaskChip, GroupLabel, taskColumns, TONE_TEXT, HUE_EDGE, HUE_BG, TaskDragProvider, SortableGroup, useSortableTask, useDraggableTask, useDropTarget, DragHandle, DropLine, LIFTED, useReducedMotion, arrayMove, isDay, dayNumber, fromDayNumber, dayOf, minutesOf, withTime, todayDay, addDays, diffDays, weekdayOf, startOfWeek, endOfWeek, weekDays, startOfMonth, endOfMonth, addMonths, daysBetween, monthGrid, WEEKEND, isWeekend, isWorkingDay, workingDaysBetween, addWorkingDays, spanOf, spansOverlap, tasksInRange, tasksOnDay, shiftDates, spanPatch, dueBucket, DUE_BUCKETS, dayForBucket, formatDay, formatTime, formatDate, timeScale, rangeOfTasks, isNewId } from \"@/components/makersclaw/blocks/task-model\". Props and an example: components/makersclaw/blocks/task-model/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
92
92
  "meta": {
93
93
  "makersclaw": {
94
- "version": "0.6.0",
94
+ "version": "0.8.0",
95
95
  "useWhen": "You are building a view of work items — a list, calendar, gantt, workload, roadmap or a switcher between them — or driving Board / DataTable from tasks. Views import it; a surface rarely renders it alone. Not for records that are not work (use data-table or board directly)."
96
96
  }
97
97
  }
@@ -5,7 +5,7 @@
5
5
  "title": "Testimonials",
6
6
  "description": "A marketing section of customer quotes: name, role, company, face or logo, the quote, an optional star rating, a verified mark and a \"view on X\" source, and optional headline numbers (stats). Layouts: a grid of cards (plain, card or raised; hover can light one and dim the rest; a \"write a review\" tile), one large quote, a switcher of faces, and a carousel, optionally over a picture.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -45,7 +45,7 @@
45
45
  "docs": "Installed to components/makersclaw/blocks/testimonials. Import: import { Testimonials } from \"@/components/makersclaw/blocks/testimonials\". Props and an example: components/makersclaw/blocks/testimonials/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
46
46
  "meta": {
47
47
  "makersclaw": {
48
- "version": "0.6.0",
48
+ "version": "0.8.0",
49
49
  "useWhen": "Social proof on a public page: a landing page, a pricing page, a product page, a case-study teaser. Not for reviews a signed-in user writes and manages (a record list or form), or an app's own activity feed."
50
50
  }
51
51
  }
@@ -5,7 +5,7 @@
5
5
  "title": "TransactionList",
6
6
  "description": "Money records as a plain list, in two variants: history (payments in and out, grouped by month, a received / paid / pending / failed mark and a signed amount) and invoices (number, date, a Paid / Due in N days / N days overdue badge, amount, download). Optional date-range filter, Export and per-row download. Empty, loading and error states built in.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json"
9
9
  ],
10
10
  "dependencies": [],
11
11
  "files": [
@@ -25,7 +25,7 @@
25
25
  "path": "registry/blocks/transaction-list/README.md",
26
26
  "type": "registry:file",
27
27
  "target": "@components/makersclaw/blocks/transaction-list/README.md",
28
- "content": "# TransactionList\n\nMoney records as a plain list in a card. There are two variants:\n\n- `history` (the default) — payments in and out, grouped by month. Each\n row has a mark (received, paid, pending, failed, refunded), the name,\n the date and a signed amount.\n- `invoices` — one row per invoice: its number and date, a badge (Paid,\n Due in 12 days, Due today, 3 days overdue, Void), the amount and a\n download button.\n\nThe header can hold a date-range filter and an Export button. It is a\nlist, not a table: there are no columns and no sorting, so the rows show\nin the order they come in (newest first, usually).\n\n```jsx\nimport { TransactionList } from \"../components/blocks/transaction-list\"; // a Makersclaw app (\"./blocks/transaction-list\" from components/)\nimport { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nA history row:\n\n```js\n{ id: \"tx_1\", name: \"Northwind Ltd\", date: \"2026-09-21T12:30:00Z\", amount: 2500, direction: \"in\", status: \"completed\", description: \"Invoice #1042\" }\n```\n\n- `date` — `\"YYYY-MM-DD\"`, or an ISO timestamp (then the time shows too).\n Months group by its first seven characters.\n- `amount` — a number in the currency's major unit. The sign is drawn\n from `direction` (`\"in\"` · `\"out\"`); without one, a negative amount is\n money out.\n- `status` — `completed` (the default), `pending`, `failed` (struck\n through), `refunded`.\n- `description` — a short line before the date. `currency` — per row,\n over the `currency` prop.\n\nAn invoice row:\n\n```js\n{ id: \"inv_24\", number: \"INV-0024\", date: \"2026-09-15\", dueDate: \"2026-10-15\", amount: 2500, status: \"open\" }\n```\n\n- `status` — `paid`, `open` (then the badge counts the days to or past\n `dueDate`), `overdue` (when there is no due date), `void`, `draft`,\n `cancelled`.\n- `name` — who it is for; shown before the date when `number` is set.\n\n## Props\n\n- `rows` — the records, in the order to show.\n- `variant` — `\"history\"` (default) or `\"invoices\"`.\n- `currency` — default `\"USD\"`. `locale` — money and dates, default the\n reader's.\n- `groupBy` — `\"month\"` (the default for history) or `\"none\"` (the\n default for invoices).\n- `title` · `description` — the card's heading; default \"Transactions\" or\n \"Invoices\".\n- `dateFilter` — `true` shows a date-range picker in the header (with\n presets). The block hides rows outside the range itself; `range` +\n `onRangeChange(range)` control it (`{ from, to }` or `null`) when the\n surface loads by date instead.\n- `onExport(rows, range)` — an Export button; it gets the rows on screen.\n Return `{ ok: false, error }` to say why it failed.\n- `onDownload(row)` — a download button on each row (make the PDF, open\n it); same return, the reason shows under the row. Or\n `downloadHref(row)` — a link instead (return `null` for a row with none).\n- `onOpen(row)` — the row's name becomes a button (open your `Sheet`).\n- `loading` — skeleton rows. `error` — a line above the list.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `today` — `\"YYYY-MM-DD\"`, the day an invoice's due date is counted\n from; default today.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// components/Billing.jsx — client\n\"use client\";\nimport { TransactionList } from \"./blocks/transaction-list\";\n\nexport default function Billing({ invoices, payments, error }) {\n return (\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <TransactionList\n variant=\"invoices\"\n rows={invoices}\n error={error}\n description=\"Every invoice we have sent you.\"\n downloadHref={(inv) => inv.pdfUrl}\n />\n <TransactionList\n rows={payments}\n dateFilter\n description=\"Money in and out of the account.\"\n onExport={async (rows) => {\n const csv = rows.map((r) => [r.date, r.name, r.amount].join(\",\")).join(\"\\n\");\n const url = URL.createObjectURL(new Blob([csv], { type: \"text/csv\" }));\n Object.assign(document.createElement(\"a\"), { href: url, download: \"payments.csv\" }).click();\n return { ok: true };\n }}\n />\n </div>\n );\n}\n```\n\n## What it will not do\n\n- Sort, page, or filter by column: rows come in ordered, and a record set\n that needs those is a `data-table`.\n- Total the rows: put a `Stat` above it if a sum matters.\n- Make the invoice PDF or the export file: `onDownload` and `onExport`\n are the app's.\n\nA list it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add transaction-list` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
28
+ "content": "# TransactionList\n\nMoney records as a plain list in a card. There are two variants:\n\n- `history` (the default) — payments in and out, grouped by month. Each\n row has a mark (received, paid, pending, failed, refunded), the name,\n the date and a signed amount.\n- `invoices` — one row per invoice: its number and date, a badge (Paid,\n Due in 12 days, Due today, 3 days overdue, Void), the amount and a\n download button.\n\nThe header can hold a date-range filter and an Export button. It is a\nlist, not a table: there are no columns and no sorting, so the rows show\nin the order they come in (newest first, usually).\n\n```jsx\nimport { TransactionList } from \"../components/blocks/transaction-list\"; // a Makersclaw app (\"./blocks/transaction-list\" from components/)\nimport { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component, and\nload the rows in the server surface (README §4).\n\n## The data\n\nA history row:\n\n```js\n{ id: \"tx_1\", name: \"Northwind Ltd\", date: \"2026-09-21T12:30:00Z\", amount: 2500, direction: \"in\", status: \"completed\", description: \"Invoice #1042\" }\n```\n\n- `date` — `\"YYYY-MM-DD\"`, or an ISO timestamp (then the time shows too).\n Months group by its first seven characters.\n- `amount` — a number in the currency's major unit. The sign is drawn\n from `direction` (`\"in\"` · `\"out\"`); without one, a negative amount is\n money out.\n- `status` — `completed` (the default), `pending`, `failed` (struck\n through), `refunded`.\n- `description` — a short line before the date. `currency` — per row,\n over the `currency` prop.\n\nAn invoice row:\n\n```js\n{ id: \"inv_24\", number: \"INV-0024\", date: \"2026-09-15\", dueDate: \"2026-10-15\", amount: 2500, status: \"open\" }\n```\n\n- `status` — `paid`, `open` (then the badge counts the days to or past\n `dueDate`), `overdue` (when there is no due date), `void`, `draft`,\n `cancelled`.\n- `name` — who it is for; shown before the date when `number` is set.\n\n## Props\n\n- `rows` — the records, in the order to show.\n- `variant` — `\"history\"` (default) or `\"invoices\"`.\n- `currency` — default `\"USD\"`. `locale` — money and dates, default the\n reader's.\n- `groupBy` — `\"month\"` (the default for history) or `\"none\"` (the\n default for invoices).\n- `title` · `description` — the card's heading; default \"Transactions\" or\n \"Invoices\".\n- `dateFilter` — `true` shows a date-range picker in the header (with\n presets). The block hides rows outside the range itself; `range` +\n `onRangeChange(range)` control it (`{ from, to }` or `null`) when the\n surface loads by date instead.\n- `onExport(rows, range)` — an Export button; it gets the rows on screen.\n Return `{ ok: false, error }` to say why it failed.\n- `onDownload(row)` — a download button on each row (make the PDF, open\n it); same return, the reason shows under the row. Or\n `downloadHref(row)` — a link instead (return `null` for a row with none).\n- `onOpen(row)` — the row's name becomes a button (open your `Sheet`).\n- `loading` — skeleton rows. `error` — a line above the list.\n- `emptyTitle` · `emptyBody` — the empty state's words.\n- `today` — `\"YYYY-MM-DD\"`, the day an invoice's due date is counted\n from; default today.\n- `className` — layout around it (width, margin). Never colour.\n\n## Example\n\n```jsx\n// surfaces/billing.jsx — client: invoices and payments\n\"use client\";\nimport { useRows } from \"../lib/data\";\nimport { TransactionList } from \"../components/blocks/transaction-list\";\n\nexport default function Billing() {\n const invoices = useRows({ source: \"invoices\", orderBy: \"date desc\" });\n const payments = useRows({ source: \"payments\", orderBy: \"date desc\" });\n return (\n <div className=\"grid gap-6 lg:grid-cols-2\">\n <TransactionList\n variant=\"invoices\"\n rows={invoices.rows}\n loading={invoices.loading}\n error={invoices.error}\n description=\"Every invoice we have sent you.\"\n downloadHref={(inv) => inv.pdf_url}\n />\n <TransactionList\n rows={payments.rows}\n loading={payments.loading}\n error={payments.error}\n dateFilter\n description=\"Money in and out of the account.\"\n onExport={async (rows) => {\n const csv = rows.map((r) => [r.date, r.name, r.amount].join(\",\")).join(\"\\n\");\n const url = URL.createObjectURL(new Blob([csv], { type: \"text/csv\" }));\n Object.assign(document.createElement(\"a\"), { href: url, download: \"payments.csv\" }).click();\n return { ok: true };\n }}\n />\n </div>\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\n## What it will not do\n\n- Sort, page, or filter by column: rows come in ordered, and a record set\n that needs those is a `data-table`.\n- Total the rows: put a `Stat` above it if a sum matters.\n- Make the invoice PDF or the export file: `onDownload` and `onExport`\n are the app's.\n\nA list it cannot be is your own component in `components/`, built from\nthe kit — not an edit to this block. Its files are fixed: `npm run check`\nrefuses a change, and `makersclaw add transaction-list` restores them.\n\n## Credits\n\nShape and behaviour from Creative Tim UI (https://github.com/creativetimofficial/ui), MIT; this is a rewrite on the Makersclaw kit.\n"
29
29
  }
30
30
  ],
31
31
  "categories": [
@@ -46,7 +46,7 @@
46
46
  "docs": "Installed to components/makersclaw/blocks/transaction-list. Import: import { TransactionList } from \"@/components/makersclaw/blocks/transaction-list\". Props and an example: components/makersclaw/blocks/transaction-list/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
47
47
  "meta": {
48
48
  "makersclaw": {
49
- "version": "0.6.0",
49
+ "version": "0.8.0",
50
50
  "useWhen": "A billing page's invoices, an account's payment history, a wallet's recent activity: records read top to bottom, newest first. Not for records the founder sorts, filters by column or totals (data-table), or for one order (order-detail)."
51
51
  }
52
52
  }
package/r/whiteboard.json CHANGED
@@ -5,8 +5,8 @@
5
5
  "title": "Whiteboard",
6
6
  "description": "A full-page freeform whiteboard on sketch-canvas: a top bar with the board's title (click to rename), the save state (Saving…, Saved, or the error with Retry), a Board / Text view switch, Export PNG and an optional Share button; under it the Miro-style canvas (tool rail, floating toolbar, zoom, undo). An empty board offers starter templates — blank, a grid of titled areas, a 2×2 matrix — and the Text view lists every word on the board for a screen reader or an agent. With onCreateTask, a selected note or text becomes a task from the top bar (\"Create task\", editable title; several notes make one task each), and a small badge marks it on the board from then on. The sketch is plain JSON; saves are async callbacks answering { ok }.",
7
7
  "registryDependencies": [
8
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/core.json",
9
- "https://cdn.jsdelivr.net/npm/makersclaw@0.6.0/r/sketch-canvas.json"
8
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/core.json",
9
+ "https://cdn.jsdelivr.net/npm/makersclaw@0.8.0/r/sketch-canvas.json"
10
10
  ],
11
11
  "dependencies": [],
12
12
  "files": [
@@ -32,7 +32,7 @@
32
32
  "path": "registry/blocks/whiteboard/README.md",
33
33
  "type": "registry:file",
34
34
  "target": "@components/makersclaw/blocks/whiteboard/README.md",
35
- "content": "# Whiteboard\n\nA full-page board to think on, friendly like Miro. A top bar holds the\nboard's title (click it to rename), the save state, a Board / Text view\nswitch, Export PNG and, when you give it `onShare`, a Share button. Under\nit is the canvas from `sketch-canvas`: the tool rail (a bottom bar on a\nphone), the toolbar that floats over a selection, zoom, undo and redo.\n\nGive it `onCreateTask` and a selected note or text becomes a task from\nthe top bar; the note keeps a small **Task** badge from then on.\n\nAn empty board offers three starting points: **Blank**, **Grid** (six\ntitled areas, three by two) and **2×2 matrix** (impact against effort,\nfour tinted quadrants with axes). The **Text view** lists every word on\nthe board, grouped by frame — the reading of the board for a screen\nreader, and what you hand an agent.\n\n```jsx\nimport { Whiteboard } from \"../components/blocks/whiteboard\"; // a Makersclaw app (\"./blocks/whiteboard\" from components/)\nimport { Whiteboard } from \"@/components/makersclaw/blocks/whiteboard\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n`makersclaw add whiteboard` adds `sketch-canvas` with it (and its npm\npackage, `@excalidraw/excalidraw`: run `npm install` after). The helpers\nfor reading a board — `sketchToText`, `sketchItems`, `isSketchEmpty` —\ncome from `sketch-canvas`.\n\n## The data\n\nA board is two things you store: its **title** (a string) and its\n**sketch** (plain JSON, a `jsonb` column) — the sketch format is\n`sketch-canvas`'s (its README, \"The data\"). Colours in a sketch are theme\ntokens, so a board repaints when the workspace theme changes.\n\nOne table does it: `boards(id, title text, sketch jsonb, sketch_text text,\nupdated_at)`. Keep `sketch_text` (`sketchToText(sketch)`) for search and\nfor agents.\n\n## Props\n\n- `title` — the board's name, controlled. Change it when `onRename`\n answers ok.\n- `defaultTitle` — the name, uncontrolled (default \"Untitled board\").\n- `onRename(title)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called when the reader renames the board (Enter\n or leaving the field; Esc puts the old name back). While it runs the\n new name shows; a failure keeps it and shows the error with Retry.\n Without it the title still edits, in memory.\n- `value` — the sketch, controlled (see sketch-canvas: pass back exactly\n what `onChange` gave you).\n- `defaultValue` — the sketch, uncontrolled. With neither, the board\n starts empty and shows the templates.\n- `onChange(sketch)` — the whole sketch after an edit settles (debounced).\n- `onSave(sketch)` — `async`, `{ ok }`. Called with every settled edit and\n on Cmd/Ctrl+S. The top bar shows \"Saving…\", \"Saved\", or the error with a\n Retry. A throw counts as a failure.\n- `changeDelay` — ms of quiet before `onChange` / `onSave` (default 600).\n- `templates` — the starting points an empty board offers (default\n `WHITEBOARD_TEMPLATES`: blank, grid, matrix). Each is\n `{ id, label, description?, create(), preview?, icon? }`: `create()`\n returns a sketch (or an element array) to lay on the board, or `null`\n for \"start blank\". `preview` is a small node drawn in the tile (else an\n `icon`, else a frame glyph; the three built-ins draw their own). `[]`\n shows sketch-canvas's plain hint instead.\n- `onShare()` — shows a Share button (primary) that calls it: open your\n `share-dialog`. Without it there is no Share button. `shareLabel`\n renames it (default \"Share\").\n- `onExport(blob, { title, fileName })` — `async`, `{ ok }`: take the PNG\n yourself (attach it, upload it). Without it, Export downloads\n `<title>.png`. Either way a failure shows on the button.\n- `onCreateTask({ title, sourceIds })` — turns notes into tasks. With it,\n selecting a note (a shape with words in it) or a text shows **Create\n task** in the top bar: a small form with the words as the title, ready\n to edit (Enter creates). Several selected notes make one task each.\n `async`, return `{ ok: true, id? }` or `{ ok: false, error? }` (a throw\n counts as failed). `sourceIds` are the board elements it came from (the\n note and its words). While it runs the note shows \"Creating…\"; on\n success the note keeps a **Task** badge (stored in the sketch, as\n `customData.task: { id, title }` on those elements) and is not offered\n again; a failure says so and leaves the note as it was. `taskLabel`\n renames the button (default \"Create task\").\n- `onOpenTask({ id, title, sourceIds })` — makes each Task badge (and the\n success toast) a link: open the task. Without it the badges only mark.\n- `onBack()` — shows a back arrow at the left of the top bar; `backLabel`\n is its name (default \"Back\").\n- `meta` — a short node beside the title on wide screens (\"Edited by Priya\n Raman\", \"View only\").\n- `readOnly` — `false` (default). `true`: the title is plain text, the\n canvas pans and zooms but does not edit, no templates.\n- `defaultView` — `\"board\"` (default) or `\"text\"`: which view opens.\n- `canvasRef` — a ref (`useSketchCanvas()` from sketch-canvas) that\n receives the canvas handle: `exportImage()`, `getScene()`,\n `insertElements()`… (sketch-canvas README, \"The handle\").\n- `className` — layout: width, margin, height. The default height is\n `h-[40rem]`; a height class of yours (`h-dvh` for a full page, `h-full`,\n `flex-1`) replaces it. Never colour.\n\n### Templates\n\n`WHITEBOARD_TEMPLATES` is the default list. `gridSketch(opts)` and\n`matrixSketch(opts)` build the two drawn ones, so your own templates can\nreuse them:\n\n- `gridSketch({ columns = 3, rows = 2, width = 360, height = 260, gap = 48, names })`\n — titled frames; `names` overrides \"Area 1\", \"Area 2\"…\n- `matrixSketch({ x = \"Effort\", y = \"Impact\", quadrants = [\"Quick wins\", \"Big bets\", \"Fill-ins\", \"Not now\"], size = 360 })`\n — four locked, tinted quadrants (notes land on them without moving\n them), two axes, the quadrant names as editable text.\n\n## Example\n\nA board page that saves itself:\n\n```jsx\n\"use client\";\nimport { useState } from \"react\";\nimport { Whiteboard } from \"./blocks/whiteboard\";\nimport { sketchToText } from \"./blocks/sketch-canvas\";\nimport { updateRow } from \"../lib/actions\";\n\nexport default function BoardPage({ board, onShare }) {\n const [title, setTitle] = useState(board.title);\n return (\n <Whiteboard\n title={title}\n onRename={async (next) => {\n const res = await updateRow({ source: \"public.boards\", id: board.id, patch: { title: next } });\n if (res.ok) setTitle(next);\n return res;\n }}\n defaultValue={board.sketch} // jsonb, may be null\n onSave={(sketch) => updateRow({ source: \"public.boards\", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}\n onShare={onShare}\n className=\"h-dvh rounded-none border-0\"\n />\n );\n}\n```\n\nNotes that become tasks (a `tasks` table of your own):\n\n```jsx\n<Whiteboard\n defaultValue={board.sketch}\n onSave={save}\n onCreateTask={({ title, sourceIds }) => insertRow({ source: \"public.tasks\", values: { title, board_id: board.id, source_ids: sourceIds } })}\n onOpenTask={(task) => router.push(`/tasks/${task.id}`)}\n/>\n```\n\n`insertRow` answers `{ ok, id }`; the id is what the badge keeps.\n\nA retro template of your own, beside the built-in ones:\n\n```jsx\nimport { WHITEBOARD_TEMPLATES, gridSketch } from \"./blocks/whiteboard\";\n\nconst templates = [\n ...WHITEBOARD_TEMPLATES,\n { id: \"retro\", label: \"Retro\", description: \"Went well, to improve, actions\", icon: \"history\",\n create: () => gridSketch({ columns: 3, rows: 1, height: 520, names: [\"Went well\", \"To improve\", \"Actions\"] }) },\n];\n\n<Whiteboard templates={templates} onSave={save} />\n```\n\n## What it will not do\n\n- Collaborate live: no cursors of others, saves are last-write-wins (as\n sketch-canvas).\n- Share by itself: `onShare` is yours — open `share-dialog`, copy a link.\n- Keep a list of boards: one board per component; the list is your page.\n- Turn on snap-to-grid from a template: the canvas menu has it.\n- Show a minimap (sketch-canvas has none).\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add whiteboard` restores them.\n"
35
+ "content": "# Whiteboard\n\nA full-page board to think on, friendly like Miro. A top bar holds the\nboard's title (click it to rename), the save state, a Board / Text view\nswitch, Export PNG and, when you give it `onShare`, a Share button. Under\nit is the canvas from `sketch-canvas`: the tool rail (a bottom bar on a\nphone), the toolbar that floats over a selection, zoom, undo and redo.\n\nGive it `onCreateTask` and a selected note or text becomes a task from\nthe top bar; the note keeps a small **Task** badge from then on.\n\nAn empty board offers three starting points: **Blank**, **Grid** (six\ntitled areas, three by two) and **2×2 matrix** (impact against effort,\nfour tinted quadrants with axes). The **Text view** lists every word on\nthe board, grouped by frame — the reading of the board for a screen\nreader, and what you hand an agent.\n\n```jsx\nimport { Whiteboard } from \"../components/blocks/whiteboard\"; // a Makersclaw app (\"./blocks/whiteboard\" from components/)\nimport { Whiteboard } from \"@/components/makersclaw/blocks/whiteboard\"; // a shadcn project\n```\n\nIt is a client component: render it from a `\"use client\"` component.\n`makersclaw add whiteboard` adds `sketch-canvas` with it (and its npm\npackage, `@excalidraw/excalidraw`: run `npm install` after). The helpers\nfor reading a board — `sketchToText`, `sketchItems`, `isSketchEmpty` —\ncome from `sketch-canvas`.\n\n## The data\n\nA board is two things you store: its **title** (a string) and its\n**sketch** (plain JSON, a `jsonb` column) — the sketch format is\n`sketch-canvas`'s (its README, \"The data\"). Colours in a sketch are theme\ntokens, so a board repaints when the workspace theme changes.\n\nOne table does it: `boards(id, title text, sketch jsonb, sketch_text text,\nupdated_at)`. Keep `sketch_text` (`sketchToText(sketch)`) for search and\nfor agents.\n\n## Props\n\n- `title` — the board's name, controlled. Change it when `onRename`\n answers ok.\n- `defaultTitle` — the name, uncontrolled (default \"Untitled board\").\n- `onRename(title)` — `async`, returns `{ ok: true }` or\n `{ ok: false, error? }`. Called when the reader renames the board (Enter\n or leaving the field; Esc puts the old name back). While it runs the\n new name shows; a failure keeps it and shows the error with Retry.\n Without it the title still edits, in memory.\n- `value` — the sketch, controlled (see sketch-canvas: pass back exactly\n what `onChange` gave you).\n- `defaultValue` — the sketch, uncontrolled. With neither, the board\n starts empty and shows the templates.\n- `onChange(sketch)` — the whole sketch after an edit settles (debounced).\n- `onSave(sketch)` — `async`, `{ ok }`. Called with every settled edit and\n on Cmd/Ctrl+S. The top bar shows \"Saving…\", \"Saved\", or the error with a\n Retry. A throw counts as a failure.\n- `changeDelay` — ms of quiet before `onChange` / `onSave` (default 600).\n- `templates` — the starting points an empty board offers (default\n `WHITEBOARD_TEMPLATES`: blank, grid, matrix). Each is\n `{ id, label, description?, create(), preview?, icon? }`: `create()`\n returns a sketch (or an element array) to lay on the board, or `null`\n for \"start blank\". `preview` is a small node drawn in the tile (else an\n `icon`, else a frame glyph; the three built-ins draw their own). `[]`\n shows sketch-canvas's plain hint instead.\n- `onShare()` — shows a Share button (primary) that calls it: open your\n `share-dialog`. Without it there is no Share button. `shareLabel`\n renames it (default \"Share\").\n- `onExport(blob, { title, fileName })` — `async`, `{ ok }`: take the PNG\n yourself (attach it, upload it). Without it, Export downloads\n `<title>.png`. Either way a failure shows on the button.\n- `onCreateTask({ title, sourceIds })` — turns notes into tasks. With it,\n selecting a note (a shape with words in it) or a text shows **Create\n task** in the top bar: a small form with the words as the title, ready\n to edit (Enter creates). Several selected notes make one task each.\n `async`, return `{ ok: true, id? }` or `{ ok: false, error? }` (a throw\n counts as failed). `sourceIds` are the board elements it came from (the\n note and its words). While it runs the note shows \"Creating…\"; on\n success the note keeps a **Task** badge (stored in the sketch, as\n `customData.task: { id, title }` on those elements) and is not offered\n again; a failure says so and leaves the note as it was. `taskLabel`\n renames the button (default \"Create task\").\n- `onOpenTask({ id, title, sourceIds })` — makes each Task badge (and the\n success toast) a link: open the task. Without it the badges only mark.\n- `onBack()` — shows a back arrow at the left of the top bar; `backLabel`\n is its name (default \"Back\").\n- `meta` — a short node beside the title on wide screens (\"Edited by Priya\n Raman\", \"View only\").\n- `readOnly` — `false` (default). `true`: the title is plain text, the\n canvas pans and zooms but does not edit, no templates.\n- `defaultView` — `\"board\"` (default) or `\"text\"`: which view opens.\n- `canvasRef` — a ref (`useSketchCanvas()` from sketch-canvas) that\n receives the canvas handle: `exportImage()`, `getScene()`,\n `insertElements()`… (sketch-canvas README, \"The handle\").\n- `className` — layout: width, margin, height. The default height is\n `h-[40rem]`; a height class of yours (`h-full`, `min-h-0 flex-1`)\n replaces it. A board that is the whole surface sits in an `AppLayout\n contentMode=\"fill\"` from the kit with `min-h-0 flex-1` (the Example):\n never `h-dvh` or a `vh` height, which `npm run check` refuses. Never\n colour.\n\n### Templates\n\n`WHITEBOARD_TEMPLATES` is the default list. `gridSketch(opts)` and\n`matrixSketch(opts)` build the two drawn ones, so your own templates can\nreuse them:\n\n- `gridSketch({ columns = 3, rows = 2, width = 360, height = 260, gap = 48, names })`\n — titled frames; `names` overrides \"Area 1\", \"Area 2\"…\n- `matrixSketch({ x = \"Effort\", y = \"Impact\", quadrants = [\"Quick wins\", \"Big bets\", \"Fill-ins\", \"Not now\"], size = 360 })`\n — four locked, tinted quadrants (notes land on them without moving\n them), two axes, the quadrant names as editable text.\n\n## Example\n\nA board page that saves itself:\n\n```jsx\n// surfaces/board.jsx — client: one board, saved as it changes\n\"use client\";\nimport { useSearchParams } from \"next/navigation\";\nimport { useRow, whenLanded } from \"../lib/data\";\nimport { updateRow } from \"../lib/actions\";\nimport { AppLayout } from \"../components/ui\";\nimport { Whiteboard } from \"../components/blocks/whiteboard\";\nimport { sketchToText } from \"../components/blocks/sketch-canvas\";\n\nexport default function BoardPage() {\n const id = useSearchParams().get(\"row\");\n const { row: board, loading } = useRow({ source: \"boards\", id });\n if (loading || !board) return null;\n return (\n <AppLayout contentMode=\"fill\">\n <Whiteboard\n key={board.id}\n title={board.title}\n onRename={async (next) => {\n const res = await updateRow({ source: \"boards\", id: board.id, patch: { title: next } });\n if (res.ok) await whenLanded(); // the board is read again: the title is the new one\n return res;\n }}\n defaultValue={board.sketch} // jsonb, may be null; read once per board\n onSave={(sketch) => updateRow({ source: \"boards\", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}\n className=\"min-h-0 flex-1 rounded-none border-0\"\n />\n </AppLayout>\n );\n}\n```\n\nIn a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.\n\nNotes that become tasks (a `tasks` table of your own):\n\n```jsx\n<Whiteboard\n defaultValue={board.sketch}\n onSave={save}\n onCreateTask={({ title, sourceIds }) => insertRow({ source: \"tasks\", values: { title, board_id: board.id, source_ids: sourceIds } })}\n onOpenTask={(task) => router.push(`/s/tasks?row=${task.id}`)}\n/>\n```\n\n`insertRow` answers `{ ok, id }`; the id is what the badge keeps.\n\nA retro template of your own, beside the built-in ones:\n\n```jsx\nimport { WHITEBOARD_TEMPLATES, gridSketch } from \"./blocks/whiteboard\";\n\nconst templates = [\n ...WHITEBOARD_TEMPLATES,\n { id: \"retro\", label: \"Retro\", description: \"Went well, to improve, actions\", icon: \"history\",\n create: () => gridSketch({ columns: 3, rows: 1, height: 520, names: [\"Went well\", \"To improve\", \"Actions\"] }) },\n];\n\n<Whiteboard templates={templates} onSave={save} />\n```\n\n## What it will not do\n\n- Collaborate live: no cursors of others, saves are last-write-wins (as\n sketch-canvas).\n- Share by itself: `onShare` is yours — open `share-dialog`, copy a link.\n- Keep a list of boards: one board per component; the list is your page.\n- Turn on snap-to-grid from a template: the canvas menu has it.\n- Show a minimap (sketch-canvas has none).\n\nA board it cannot be is your own component in `components/` — not an edit\nto this block. Its files are fixed: `npm run check` refuses a change, and\n`makersclaw add whiteboard` restores them.\n"
36
36
  }
37
37
  ],
38
38
  "categories": [
@@ -59,7 +59,7 @@
59
59
  "docs": "Installed to components/makersclaw/blocks/whiteboard. Import: import { Whiteboard, WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"@/components/makersclaw/blocks/whiteboard\". Props and an example: components/makersclaw/blocks/whiteboard/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
60
60
  "meta": {
61
61
  "makersclaw": {
62
- "version": "0.6.0",
62
+ "version": "0.8.0",
63
63
  "useWhen": "A screen is one board to think on: a team whiteboard, a planning or prioritisation board, a place to sketch an idea and keep it with a record. Not for marking up a screenshot (annotate), sketching a UI to build (sketch-to-app), a structured board of cards and connectors (flow-canvas), or a kanban (board)."
64
64
  }
65
65
  }
@@ -140,26 +140,38 @@ a partial one:
140
140
  A feedback widget that saves itself and files a ticket:
141
141
 
142
142
  ```jsx
143
+ // surfaces/feedback.jsx — client: one ticket, annotated
143
144
  "use client";
144
- import { Annotate } from "./blocks/annotate";
145
+ import { useSearchParams } from "next/navigation";
146
+ import { useRow } from "../lib/data";
145
147
  import { insertRow, updateRow } from "../lib/actions";
148
+ import { AppLayout } from "../components/ui";
149
+ import { Annotate } from "../components/blocks/annotate";
146
150
 
147
- export default function Feedback({ ticket }) {
151
+ export default function Feedback() {
152
+ const id = useSearchParams().get("row");
153
+ const { row: ticket, loading } = useRow({ source: "tickets", id });
154
+ if (loading || !ticket) return null;
148
155
  return (
149
- <Annotate
150
- title="What should change?"
151
- defaultValue={ticket.annotation} // jsonb, may be null
152
- onSave={(annotation) => updateRow({ source: "public.tickets", id: ticket.id, patch: { annotation } })}
153
- onSubmit={({ notes, text }) => // image: a PNG Blob, to upload if you keep it
154
- insertRow({ source: "public.bug_reports", values: { ticket_id: ticket.id, summary: text, note_count: notes.length } })
155
- }
156
- submitLabel="File the bug"
157
- className="h-dvh"
158
- />
156
+ <AppLayout contentMode="fill">
157
+ <Annotate
158
+ key={ticket.id}
159
+ title="What should change?"
160
+ defaultValue={ticket.annotation} // jsonb, may be null; read once per ticket
161
+ onSave={(annotation) => updateRow({ source: "tickets", id: ticket.id, patch: { annotation } })}
162
+ onSubmit={({ notes, text }) => // image: a PNG Blob, to upload if you keep it
163
+ insertRow({ source: "bug_reports", values: { ticket_id: ticket.id, summary: text, note_count: notes.length } })
164
+ }
165
+ submitLabel="File the bug"
166
+ className="min-h-0 flex-1"
167
+ />
168
+ </AppLayout>
159
169
  );
160
170
  }
161
171
  ```
162
172
 
173
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
174
+
163
175
  ## What it will not do
164
176
 
165
177
  - Crop, rotate or resize the screenshot. Replace swaps it for another;
@@ -62,20 +62,22 @@ and pass its fields in.
62
62
  ## Example
63
63
 
64
64
  ```jsx
65
- // components/PurchaseApproval.jsx — client
65
+ // surfaces/approval.jsx — client: one request, read and decided
66
66
  "use client";
67
- import { useRouter } from "next/navigation";
67
+ import { useSearchParams } from "next/navigation";
68
+ import { useRow } from "../lib/data";
68
69
  import { updateRow } from "../lib/actions";
69
- import { ApprovalRequest } from "./blocks/approval-request";
70
+ import { ApprovalRequest } from "../components/blocks/approval-request";
70
71
 
71
72
  const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
72
73
 
73
- export default function PurchaseApproval({ request }) {
74
- const router = useRouter();
74
+ export default function PurchaseApproval() {
75
+ const id = useSearchParams().get("row"); // /s/approval?row=<id>
76
+ const { row: request, loading } = useRow({ source: "purchase_requests", id });
77
+ if (loading || !request) return null;
78
+ // decided_by and decided_at: a before-update trigger sets them from the member, never the page (the app's README §5)
75
79
  const decide = (status, reason) =>
76
- updateRow({ source: "public.purchase_requests", id: request.id, patch: { status, reason: reason ?? null }, detail: request.product }).then(
77
- (res) => (res.ok && router.refresh(), res),
78
- );
80
+ updateRow({ source: "purchase_requests", id: request.id, patch: { status, reason: reason ?? null }, detail: request.product });
79
81
  return (
80
82
  <ApprovalRequest
81
83
  title={request.product}
@@ -99,6 +101,11 @@ export default function PurchaseApproval({ request }) {
99
101
  }
100
102
  ```
101
103
 
104
+ A decision that answers ok reads the request again, so the card shows
105
+ it decided.
106
+
107
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
108
+
102
109
  ## What it will not do
103
110
 
104
111
  - List many requests: put them in a `data-table` and open one in this
@@ -126,17 +126,23 @@ and region labels are buttons in Tab order.
126
126
 
127
127
  ## Example
128
128
 
129
- A call recording with its topics and comments, saved with
130
- `lib/actions.js` in a Makersclaw app.
129
+ A call recording with its topics and comments, read with the hooks of
130
+ `lib/data.js` and saved with `lib/actions.js` in a Makersclaw app.
131
131
 
132
132
  ```jsx
133
+ // surfaces/call.jsx — client: one call, its topics and comments
133
134
  "use client";
134
- import { useState } from "react";
135
+ import { useSearchParams } from "next/navigation";
136
+ import { useRow, useRows, whenLanded } from "../lib/data";
135
137
  import { insertRow } from "../lib/actions";
136
- import { AudioPlayer } from "./blocks/audio-player";
137
-
138
- export default function CallRecording({ call, topics, comments: initial }) {
139
- const [comments, setComments] = useState(initial);
138
+ import { AudioPlayer } from "../components/blocks/audio-player";
139
+
140
+ export default function CallRecording() {
141
+ const id = useSearchParams().get("row");
142
+ const { row: call, loading } = useRow({ source: "calls", id });
143
+ const { rows: topics } = useRows({ source: "call_topics", filter: { call_id: id }, orderBy: "start_s asc" });
144
+ const { rows: comments } = useRows({ source: "call_comments", filter: { call_id: id }, orderBy: "at_s asc" });
145
+ if (loading || !call) return null;
140
146
  return (
141
147
  <AudioPlayer
142
148
  src={call.recording_url}
@@ -145,9 +151,8 @@ export default function CallRecording({ call, topics, comments: initial }) {
145
151
  regions={topics.map((t) => ({ id: t.id, start: t.start_s, end: t.end_s, label: t.name, tone: "chart-2" }))}
146
152
  markers={comments.map((c) => ({ id: c.id, time: c.at_s, label: c.body, tone: "primary" }))}
147
153
  onAddMarker={async ({ time }) => {
148
- const values = { call_id: call.id, at_s: time, body: "New comment" };
149
- const res = await insertRow({ source: "public.call_comments", values });
150
- if (res.ok) setComments((cs) => [...cs, { ...values, id: res.id }]);
154
+ const res = await insertRow({ source: "call_comments", values: { call_id: call.id, at_s: time, body: "New comment" } });
155
+ if (res.ok) await whenLanded(); // the comments are read again: the new marker is in them
151
156
  return res;
152
157
  }}
153
158
  />
@@ -155,6 +160,8 @@ export default function CallRecording({ call, topics, comments: initial }) {
155
160
  }
156
161
  ```
157
162
 
163
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
164
+
158
165
  ## What it will not do
159
166
 
160
167
  - Record from the microphone, or play video.
@@ -32,7 +32,7 @@ record goes missing; it can be dragged out of it, not into it.
32
32
  - `onMove(row, to, from)` — save the move; return `{ ok: true }` or
33
33
  `{ ok: false, error }` (a throw counts as failed). In a Makersclaw app it
34
34
  is one line over `moveRow` (lib/actions.js — the activity row included):
35
- `onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}`.
35
+ `onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}`.
36
36
  Without `onMove` the board only shows; nothing can be moved.
37
37
  - `idColumn` — default `"id"`.
38
38
  - `titleOf(row)` — the card's title, default `row.title ?? row.name`. It
@@ -77,12 +77,14 @@ record goes missing; it can be dragged out of it, not into it.
77
77
  ## Example
78
78
 
79
79
  ```jsx
80
- // surfaces/pipeline.jsx — server: load, hand over
81
- import { loadRows } from "../lib/data";
80
+ // surfaces/pipeline.jsx — client: read, hand over
81
+ "use client";
82
+ import { useRows } from "../lib/data";
82
83
  import Pipeline from "../components/Pipeline";
83
84
 
84
- export default async function PipelineSurface() {
85
- const { rows, error } = await loadRows({ source: "public.deals", orderBy: "updated_at desc" });
85
+ export default function PipelineSurface() {
86
+ const { rows, error, loading } = useRows({ source: "deals", orderBy: "updated_at desc" });
87
+ if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table
86
88
  return <Pipeline deals={rows} error={error} />;
87
89
  }
88
90
  ```
@@ -118,10 +120,10 @@ export default function Pipeline({ deals, error }) {
118
120
  rows={fixture.rows}
119
121
  groupBy="stage"
120
122
  columns={STAGES}
121
- onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}
123
+ onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}
122
124
  titleOf={(d) => d.name}
123
125
  limits={{ proposal: 5 }}
124
- onCreate={(name, stage) => insertRow({ source: "public.deals", values: { name, stage }, detail: name })}
126
+ onCreate={(name, stage) => insertRow({ source: "deals", values: { name, stage }, detail: name })}
125
127
  onOpen={setEditing}
126
128
  renderDetails={(d) => (
127
129
  <div className="flex items-center justify-between text-xs text-muted-foreground">
@@ -137,6 +139,8 @@ export default function Pipeline({ deals, error }) {
137
139
  }
138
140
  ```
139
141
 
142
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
143
+
140
144
  A sprint (icons, points per stage, a top edge by type) and a stacked list:
141
145
 
142
146
  ```jsx
@@ -146,7 +150,7 @@ const TYPE_HUE = { feature: 1, bug: 4, task: 2 };
146
150
  rows={stories}
147
151
  groupBy="status"
148
152
  columns={[{ key: "todo", label: "To do", icon: "inbox" }, { key: "doing", label: "In progress", icon: "zap" }, { key: "done", label: "Done", icon: "circle-check" }]}
149
- onMove={(s, to) => moveRow({ source: "public.stories", id: s.id, stateColumn: "status", to })}
153
+ onMove={(s, to) => moveRow({ source: "stories", id: s.id, stateColumn: "status", to })}
150
154
  aggregate={(list) => `${list.reduce((n, s) => n + (s.points ?? 0), 0)}pt`}
151
155
  accentOf={(s) => TYPE_HUE[s.type] ?? null}
152
156
  renderDetails={(s) => <Badge>{s.points}pt</Badge>}
@@ -159,7 +163,7 @@ const TYPE_HUE = { feature: 1, bug: 4, task: 2 };
159
163
  groupBy="state"
160
164
  columns={[{ key: "todo", label: "To do", icon: "inbox" }, { key: "done", label: "Done", icon: "circle-check" }]}
161
165
  defaultOpen={["todo"]}
162
- onMove={(c, to) => moveRow({ source: "public.chores", id: c.id, stateColumn: "state", to })}
166
+ onMove={(c, to) => moveRow({ source: "chores", id: c.id, stateColumn: "state", to })}
163
167
  metaOf={(c) => c.room}
164
168
  renderDetails={(c) => <Badge tone={c.every === "daily" ? "warning" : "info"}>{c.every}</Badge>}
165
169
  label="Chores by state"
@@ -17,7 +17,7 @@ import { Button, Collapsible, Icon, Input, Menu, Notice, cx } from "../../ui";
17
17
  // so it runs in a Makersclaw app and in any shadcn project alike.
18
18
  //
19
19
  // <Board rows={deals} groupBy="stage" columns={STAGES}
20
- // onMove={(d, to) => moveRow({ source: "public.deals", id: d.id, stateColumn: "stage", to })}
20
+ // onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}
21
21
  // titleOf={(d) => d.name} renderDetails={(d) => <DealFacts deal={d} />}
22
22
  // onOpen={setEditing} />
23
23
  //