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
@@ -157,22 +157,46 @@ const getSlots = useCallback(({ from, to }) =>
157
157
  ## Example
158
158
 
159
159
  ```jsx
160
+ // surfaces/schedule.jsx — client: the tasks on a calendar
160
161
  "use client";
161
162
  import { useState } from "react";
162
- import { Sheet } from "./ui";
163
- import { CalendarView } from "./blocks/calendar-view";
163
+ import { useRows } from "../lib/data";
164
+ import { deleteRow, insertRow, updateRow } from "../lib/actions";
165
+ import { Sheet } from "../components/ui";
166
+ import { CalendarView } from "../components/blocks/calendar-view";
167
+ import { changeWrites } from "../components/blocks/task-model";
168
+
169
+ const COLUMNS = ["id", "title", "status", "assignees", "start", "due", "project"];
170
+
171
+ // One change of the calendar's, as the rows it writes (task-list's README has the same).
172
+ async function save(change) {
173
+ for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
174
+ const r =
175
+ w.op === "insert" ? await insertRow({ source: "tasks", values: w.values, detail: w.values.title })
176
+ : w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
177
+ : await deleteRow({ source: "tasks", id: w.id });
178
+ if (r?.ok === false) return r;
179
+ }
180
+ return { ok: true };
181
+ }
164
182
 
165
- export default function Schedule({ tasks, people, projects, onChange }) {
183
+ export default function Schedule() {
184
+ const tasks = useRows({ source: "tasks", orderBy: "start asc" });
185
+ const people = useRows({ source: "people" });
186
+ const projects = useRows({ source: "projects" });
166
187
  const [open, setOpen] = useState(null);
167
188
  return (
168
189
  <>
169
- <CalendarView tasks={tasks} people={people} projects={projects} onChange={onChange} onOpen={setOpen} defaultMode="week" />
190
+ <CalendarView tasks={tasks.rows} people={people.rows} projects={projects.rows} onChange={save} onOpen={setOpen} defaultMode="week"
191
+ loading={tasks.loading || people.loading || projects.loading} error={tasks.error} />
170
192
  <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>…</Sheet>
171
193
  </>
172
194
  );
173
195
  }
174
196
  ```
175
197
 
198
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
199
+
176
200
  ## What it will not do
177
201
 
178
202
  - Recurring events, time zones, or events that are not tasks: a repeat
@@ -94,33 +94,35 @@ The checkout step:
94
94
  ## Example
95
95
 
96
96
  ```jsx
97
- // components/Basket.jsx — client
97
+ // surfaces/basket.jsx — client: the cart, read and changed
98
98
  "use client";
99
99
  import { useState } from "react";
100
+ import { useRows } from "../lib/data";
100
101
  import { deleteRow, updateRow } from "../lib/actions";
101
- import { Cart } from "./blocks/cart";
102
+ import { Cart } from "../components/blocks/cart";
102
103
 
103
- export default function Basket({ items, error }) {
104
+ export default function Basket() {
105
+ const { rows: items, error, loading } = useRows({ source: "cart_items", orderBy: "created_at asc" });
104
106
  const [step, setStep] = useState("cart");
105
107
  return (
106
108
  <Cart
107
109
  items={items}
110
+ loading={loading}
108
111
  error={error}
109
112
  step={step}
110
113
  shipping={0}
111
114
  tax="Calculated at payment"
112
115
  note="Free delivery on every order this month."
113
- onQuantityChange={(item, quantity) => updateRow({ source: "public.cart_items", id: item.id, patch: { quantity } })}
114
- onRemove={(item) => deleteRow({ source: "public.cart_items", id: item.id })}
116
+ onQuantityChange={(item, quantity) => updateRow({ source: "cart_items", id: item.id, patch: { quantity } })}
117
+ onRemove={(item) => deleteRow({ source: "cart_items", id: item.id })}
115
118
  onCheckout={() => setStep("checkout")}
116
119
  onBack={() => setStep("cart")}
117
120
  continueHref="/shop"
118
121
  terms="I agree to the terms of sale"
119
- onPay={async (details) => {
120
- const res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify(details) });
121
- if (!res.ok) return { ok: false, error: "We could not start the payment. Try again." };
122
- const { transactionId } = await res.json();
123
- window.Paddle?.Checkout.open({ transactionId });
122
+ onPay={async () => {
123
+ // Paddle.js opens its checkout in the browser from price ids: no route of the app's own.
124
+ if (!window.Paddle) return { ok: false, error: "Payments have not loaded. Try again." };
125
+ window.Paddle.Checkout.open({ items: items.map((i) => ({ priceId: i.price_id, quantity: i.quantity })) });
124
126
  return { ok: true };
125
127
  }}
126
128
  />
@@ -128,6 +130,8 @@ export default function Basket({ items, error }) {
128
130
  }
129
131
  ```
130
132
 
133
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
134
+
131
135
  ## What it will not do
132
136
 
133
137
  - Take a card number, expiry or CVC. Payment is Paddle's overlay or page,
@@ -244,15 +244,20 @@ sharing one key, say).
244
244
  ## Example
245
245
 
246
246
  ```jsx
247
- // surfaces/sales.jsx — server: load, hand over
248
- import { loadRows } from "../lib/data";
247
+ // surfaces/sales.jsx — client: read, hand over
248
+ "use client";
249
+ import { useRows } from "../lib/data";
249
250
  import SalesCharts from "../components/SalesCharts";
250
251
 
251
- export default async function SalesSurface() {
252
- const { rows: months, error } = await loadRows({ source: "public.revenue_by_month", orderBy: "month asc" });
253
- const { rows: stages } = await loadRows({ source: "public.deals_by_stage", orderBy: "position asc" });
254
- const { rows: visits } = await loadRows({ source: "public.visits_by_slot", orderBy: "position asc" });
255
- return <SalesCharts months={months} stages={stages} visits={visits} error={error} />;
252
+ export default function SalesSurface() {
253
+ const months = useRows({ source: "revenue_by_month", orderBy: "month asc" });
254
+ const stages = useRows({ source: "deals_by_stage", orderBy: "position asc" });
255
+ const visits = useRows({ source: "visits_by_slot", orderBy: "position asc" });
256
+ if (months.loading || stages.loading || visits.loading) return null;
257
+ return (
258
+ <SalesCharts months={months.rows} stages={stages.rows} visits={visits.rows}
259
+ error={months.error ?? stages.error ?? visits.error} />
260
+ );
256
261
  }
257
262
  ```
258
263
 
@@ -303,6 +308,8 @@ export default function SalesCharts({ months, stages, visits, error }) {
303
308
  }
304
309
  ```
305
310
 
311
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
312
+
306
313
  ## What it will not do
307
314
 
308
315
  - Sort, group or sum: the rows come shaped. Totals by month or by stage
@@ -523,7 +523,7 @@ function Cartesian({
523
523
  });
524
524
 
525
525
  return (
526
- <div className={cx("grid min-w-0 gap-3", className)}>
526
+ <div className={cx("relative grid min-w-0 gap-3", className)}>
527
527
  <div
528
528
  ref={boxRef}
529
529
  tabIndex={0}
@@ -244,32 +244,42 @@ export function TooltipBox({ at, title, rows, footer }) {
244
244
  }
245
245
 
246
246
  /** The numbers behind the picture, for a screen reader: a real table,
247
- * visually hidden. `head` the column names, `body` rows of strings. */
247
+ * visually hidden. `head` the column names, `body` rows of strings.
248
+ *
249
+ * The hiding is a div's, never the table's: a table takes `width: 1px`
250
+ * as a least width and grows to its rows, and a browser need not clip a
251
+ * table's overflow, so `sr-only` on the table itself left a box as wide
252
+ * as its longest row, which pushed a phone's page past the window (the
253
+ * store's probe at 375px, 2026-10-08). The div is 1px, clips what it
254
+ * holds and is absolute inside the chart's own box (each chart's root
255
+ * is `relative`); the table in it is a plain table, read as one. */
248
256
  export function DataTable({ caption, head, body }) {
249
257
  return (
250
- <table className="sr-only">
251
- <caption>{caption}</caption>
252
- <thead>
253
- <tr>
254
- {head.map((h, i) => (
255
- <th key={i} scope="col">{h}</th>
256
- ))}
257
- </tr>
258
- </thead>
259
- <tbody>
260
- {body.map((cells, r) => (
261
- <tr key={r}>
262
- {cells.map((c, i) =>
263
- i === 0 ? (
264
- <th key={i} scope="row">{c}</th>
265
- ) : (
266
- <td key={i}>{c}</td>
267
- ),
268
- )}
258
+ <div className="sr-only">
259
+ <table>
260
+ <caption>{caption}</caption>
261
+ <thead>
262
+ <tr>
263
+ {head.map((h, i) => (
264
+ <th key={i} scope="col">{h}</th>
265
+ ))}
269
266
  </tr>
270
- ))}
271
- </tbody>
272
- </table>
267
+ </thead>
268
+ <tbody>
269
+ {body.map((cells, r) => (
270
+ <tr key={r}>
271
+ {cells.map((c, i) =>
272
+ i === 0 ? (
273
+ <th key={i} scope="row">{c}</th>
274
+ ) : (
275
+ <td key={i}>{c}</td>
276
+ ),
277
+ )}
278
+ </tr>
279
+ ))}
280
+ </tbody>
281
+ </table>
282
+ </div>
273
283
  );
274
284
  }
275
285
 
@@ -168,7 +168,7 @@ export function PieChart({
168
168
  : { value: format(total), label: totalLabel };
169
169
 
170
170
  return (
171
- <div className={cx("grid min-w-0 gap-3", className)}>
171
+ <div className={cx("relative grid min-w-0 gap-3", className)}>
172
172
  <div
173
173
  ref={boxRef}
174
174
  tabIndex={0}
@@ -127,7 +127,10 @@ not run.
127
127
  has no `author`.
128
128
  - `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;
129
129
  `emptyState` replaces the whole thing.
130
- - `height` — default `"min(40rem, 80dvh)"`; the list scrolls inside it.
130
+ - `height` — default `"min(40rem, 80dvh)"` (the block's own); the list
131
+ scrolls inside it. Yours is a length (`"40rem"`) or `"100%"` in a sized
132
+ box, such as the kit's `AppLayout contentMode="fill"`: a `vh` length in
133
+ your code is refused by `npm run check`.
131
134
  - `label` — what the thread is called to a screen reader.
132
135
  - `className` — layout around it (width, margin). Never colour.
133
136
 
@@ -214,29 +217,53 @@ and the hint in its bar, Send at the end. The picker leaves out files
214
217
  ## Example
215
218
 
216
219
  ```jsx
217
- // components/SupportChat.jsx — client: one ticket's conversation
220
+ // surfaces/ticket.jsx — client: one ticket's conversation
218
221
  "use client";
219
222
  import { useState } from "react";
223
+ import { useSearchParams } from "next/navigation";
224
+ import { loadRows, useRow, useRows, whenLanded } from "../lib/data";
220
225
  import { insertRow } from "../lib/actions";
221
- import { ChatThread } from "./blocks/chat";
222
-
223
- export default function SupportChat({ ticket, initial }) {
224
- const [messages, setMessages] = useState(initial);
225
- const patch = (id, change) => setMessages((all) => all.map((m) => (m.id === id ? { ...m, ...change } : m)));
226
+ import { ChatThread } from "../components/blocks/chat";
227
+
228
+ const source = "ticket_messages";
229
+ const PAGE = 200;
230
+ const oldestFirst = (rows) => [...rows].reverse();
231
+ const asMessage = (r) => ({ id: r.id, role: r.from_agent ? "assistant" : "user", content: r.body, createdAt: r.created_at });
232
+
233
+ export default function SupportChat() {
234
+ const id = useSearchParams().get("row");
235
+ const { row: ticket } = useRow({ source: "tickets", id });
236
+ // The newest page, read again after every write.
237
+ const { rows, loading } = useRows({ source, filter: { ticket_id: id }, orderBy: "created_at desc", limit: PAGE });
238
+ const [earlier, setEarlier] = useState({ rows: [], more: true }); // what onLoadMore read, oldest first
239
+ const [sending, setSending] = useState([]); // sent, not read back yet
240
+ const seen = new Map([...earlier.rows, ...oldestFirst(rows)].map((r) => [r.id, r]));
241
+ const shown = [...seen.values()];
242
+
243
+ async function loadMore() {
244
+ const oldest = shown[0]?.created_at;
245
+ if (!oldest) return;
246
+ const r = await loadRows({ source, filter: { ticket_id: id, created_at: { lt: oldest } }, orderBy: "created_at desc", limit: PAGE });
247
+ // What is on screen is kept too: new messages push the oldest out of the page above.
248
+ if (!r.error) setEarlier({ rows: [...oldestFirst(r.rows), ...shown], more: r.rows.length === PAGE });
249
+ }
226
250
 
227
251
  async function send(text) {
228
- const id = crypto.randomUUID();
229
- const draft = { id, role: "user", content: text, createdAt: new Date().toISOString(), status: "sending" };
230
- setMessages((all) => [...all, draft]);
231
- const r = await insertRow({ source: "public.ticket_messages", values: { id, ticket_id: ticket.id, body: text }, detail: text });
232
- patch(id, r.ok ? { status: undefined } : { status: "failed", error: r.error });
252
+ const mid = crypto.randomUUID();
253
+ setSending((all) => [...all, { id: mid, role: "user", content: text, createdAt: new Date().toISOString(), status: "sending" }]);
254
+ const r = await insertRow({ source, values: { id: mid, ticket_id: id, body: text }, detail: text });
255
+ if (r.ok) await whenLanded(); // read back: it is in `rows` now
256
+ setSending((all) => (r.ok ? all.filter((m) => m.id !== mid) : all.map((m) => (m.id === mid ? { ...m, status: "failed", error: r.error } : m))));
233
257
  }
234
258
 
259
+ if (loading || !ticket) return null;
235
260
  return (
236
261
  <ChatThread
237
- messages={messages}
262
+ messages={[...shown.map(asMessage), ...sending.filter((m) => !seen.has(m.id))]}
238
263
  onSend={send}
239
- onRetry={(m) => { setMessages((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}
264
+ onRetry={(m) => { setSending((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}
265
+ onLoadMore={loadMore}
266
+ hasMore={rows.length === PAGE && earlier.more}
240
267
  assistantName={ticket.agent_name}
241
268
  userName="You"
242
269
  label={`Ticket ${ticket.number}`}
@@ -245,6 +272,12 @@ export default function SupportChat({ ticket, initial }) {
245
272
  }
246
273
  ```
247
274
 
275
+ `onLoadMore` reads the page before the oldest message shown:
276
+ `loadRows({ source, filter: { ticket_id, created_at: { lt: oldest } } })`,
277
+ the same filter in a server app's `loadRows` and the door.
278
+
279
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
280
+
248
281
  With the AI SDK in a shadcn project, `useChat()` already has the shape:
249
282
  `<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}
250
283
  streaming={status === "streaming" || status === "submitted"} onStop={stop}
@@ -233,7 +233,7 @@ function SupportDemo() {
233
233
  emptyTitle="No messages yet"
234
234
  emptyBody="Ask about billing, your workspace or an invoice — Ada answers in a few seconds."
235
235
  label="Support chat"
236
- height="min(44rem, 85dvh)"
236
+ height="44rem"
237
237
  />
238
238
  </div>
239
239
  );
@@ -412,7 +412,7 @@ function AssistantDemo() {
412
412
  emptyTitle="What should we look at?"
413
413
  emptyBody="Pip reads your orders, stock and returns. Pick a question or ask your own."
414
414
  label="Planning assistant"
415
- height="min(40rem, 85dvh)"
415
+ height="40rem"
416
416
  />
417
417
  </div>
418
418
  );
@@ -193,6 +193,10 @@ export function QueryBox({ runQuery }) {
193
193
  }
194
194
  ```
195
195
 
196
+ Running SQL a member typed is a server app's only: `runQuery` is a
197
+ Server Action of the app's own. A browser app has no such call, since no
198
+ route of the data door runs SQL a member typed.
199
+
196
200
  JSON, read-only, fixed height:
197
201
 
198
202
  ```jsx
@@ -5,7 +5,7 @@
5
5
  // as you type) or plain text; line numbers, problems drawn on the text,
6
6
  // find and replace, and a run hook on Cmd/Ctrl+Enter.
7
7
  //
8
- // Client only, one static bundle. CodeMirror needs the DOM, and the app's
8
+ // Client only, one bundle. CodeMirror needs the DOM, and the app's
9
9
  // check refuses import() (so no next/dynamic): the modules load with the
10
10
  // page, and the editor itself is created only after the first effect (the
11
11
  // mount guard). Until then — on the server and in the first client paint —
@@ -79,32 +79,39 @@ The other exports, for a thread of your own:
79
79
  ## Example
80
80
 
81
81
  ```jsx
82
- // components/TaskComments.jsx — client
82
+ // surfaces/task.jsx — client: one task's comments
83
83
  "use client";
84
- import { useState } from "react";
84
+ import { useSearchParams } from "next/navigation";
85
+ import { useArtifact } from "../lib/hooks";
86
+ import { useRows, whenLanded } from "../lib/data";
85
87
  import { insertRow } from "../lib/actions";
86
- import { CommentBox } from "./blocks/comment-box";
87
- import { docToText } from "./blocks/doc-editor";
88
-
89
- export default function TaskComments({ task, me, team, initial }) {
90
- const [comments, setComments] = useState(initial);
88
+ import { CommentBox } from "../components/blocks/comment-box";
89
+ import { docToText } from "../components/blocks/doc-editor";
90
+
91
+ export default function TaskComments() {
92
+ const taskId = useSearchParams().get("row");
93
+ const { user } = useArtifact(); // the member signed in: { id, email }
94
+ const { rows, loading } = useRows({ source: "comments", filter: { task_id: taskId }, orderBy: "created_at asc" });
95
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
96
+ // created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
97
+ const comments = rows.map((c) => ({ id: c.id, author: c.created_by, body: c.body, createdAt: c.created_at }));
91
98
  const findPeople = async (q) =>
92
99
  team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
93
100
 
94
101
  async function post(doc) {
95
- const res = await insertRow({
96
- source: "public.comments",
97
- values: { task_id: task.id, author_id: me.id, body: doc, body_text: docToText(doc) },
98
- });
102
+ const res = await insertRow({ source: "comments", values: { task_id: taskId, body: doc, body_text: docToText(doc) } });
99
103
  if (!res.ok) return { ok: false, error: res.error };
100
- setComments((c) => [...c, { id: res.id, author: me, body: doc, createdAt: new Date() }]);
104
+ await whenLanded(); // the comment is read back before the box clears
101
105
  return { ok: true };
102
106
  }
103
107
 
104
- return <CommentBox comments={comments} onPost={post} author={me} mentions={findPeople} />;
108
+ if (loading) return null;
109
+ return <CommentBox comments={comments} onPost={post} author={user ? { name: user.email } : undefined} mentions={findPeople} />;
105
110
  }
106
111
  ```
107
112
 
113
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
114
+
108
115
  ## What it will not do
109
116
 
110
117
  - Edit or delete a posted comment, react, or reply in a nested thread: the
@@ -107,10 +107,10 @@ It is a client component: render it from a `"use client"` component.
107
107
  ## Example
108
108
 
109
109
  ```jsx
110
- // components/Contact.jsx
110
+ // components/Contact.jsx — client: a message is a row
111
111
  "use client";
112
+ import { insertRow } from "../lib/actions";
112
113
  import { ContactSection } from "./blocks/contact-section";
113
- import { sendContactMessage } from "../lib/actions"; // your server action
114
114
 
115
115
  export default function Contact() {
116
116
  return (
@@ -124,15 +124,16 @@ export default function Contact() {
124
124
  ]}
125
125
  topics={{ options: ["Sales", "Support", "Partnerships"], required: true }}
126
126
  consent="I agree to the privacy policy."
127
- onSubmit={async (values) => {
128
- const res = await sendContactMessage(values);
129
- return res.ok ? { ok: true } : { ok: false, error: res.error };
130
- }}
127
+ onSubmit={(values) => insertRow({ source: "contact_messages", values })} // { ok, id } or { ok: false, error }
131
128
  />
132
129
  );
133
130
  }
134
131
  ```
135
132
 
133
+ The row is the message: the app's agent, or a job, answers it.
134
+
135
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
136
+
136
137
  ## What it will not do
137
138
 
138
139
  - Send anything: `onSubmit` is yours (save a row, email the team).
@@ -93,33 +93,55 @@ creates, fields }` (each row's `__problems` is `{ key: message }`) ·
93
93
  ## Example
94
94
 
95
95
  ```jsx
96
+ // surfaces/import.jsx — client: contacts from a spreadsheet
96
97
  "use client";
97
98
  import { useRouter } from "next/navigation";
98
- import { insertRow } from "../lib/actions";
99
- import { CsvImport } from "./blocks/csv-import";
99
+ import { useRows } from "../lib/data";
100
+ import { runAction } from "../lib/actions";
101
+ import { CsvImport } from "../components/blocks/csv-import";
100
102
 
101
- export default function ImportContacts({ fields, team }) {
103
+ const FIELDS = [
104
+ { key: "name", name: "Name", type: "text", required: true },
105
+ { key: "email", name: "Email", type: "text", required: true },
106
+ { key: "owner", name: "Owner", type: "person" },
107
+ ];
108
+
109
+ export default function ImportContacts() {
102
110
  const router = useRouter();
111
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
103
112
  return (
104
113
  <CsvImport
105
- fields={fields}
114
+ fields={FIELDS}
106
115
  people={team}
107
116
  noun="contact"
108
117
  onImport={async ({ rows }) => {
109
- let imported = 0;
110
- for (const { id, ...values } of rows) {
111
- const r = await insertRow({ source: "public.contacts", values });
112
- if (!r.ok) return { ok: false, error: `Stopped after ${imported} contacts: ${r.error}` };
113
- imported++;
114
- }
115
- return { ok: true, imported };
118
+ // One action, one transaction: a write per row would stop halfway,
119
+ // and the door takes 300 writes a minute.
120
+ const r = await runAction({ name: "import_contacts", args: { rows: rows.map(({ id, ...values }) => values) } });
121
+ return r.ok ? { ok: true, imported: r.result?.imported } : r;
116
122
  }}
117
- onDone={() => router.push("/contacts")}
123
+ onDone={() => router.push("/s/contacts")}
118
124
  />
119
125
  );
120
126
  }
121
127
  ```
122
128
 
129
+ The action, named in `references/actions.md` and made with `sql`:
130
+
131
+ ```sql
132
+ create or replace function import_contacts(args jsonb) returns jsonb
133
+ language sql as $$
134
+ with made as (
135
+ insert into contacts (name, email, owner)
136
+ select name, email, owner from jsonb_populate_recordset(null::contacts, args->'rows')
137
+ returning 1
138
+ )
139
+ select jsonb_build_object('imported', count(*)) from made
140
+ $$;
141
+ ```
142
+
143
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
144
+
123
145
  ## What it will not do
124
146
 
125
147
  - Excel files (.xlsx) are not read: save the sheet as CSV first (the
@@ -252,8 +252,19 @@ argument, `ids`, is every selected id, including rows not on this page.
252
252
 
253
253
  ```jsx
254
254
  const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSize: 25 });
255
- // …read rows + total for q on the server (a Server Action, a route)…
256
- <DataTable manual rows={pageRows} rowCount={total} loading={pending}
255
+ // One page through the door: the filter grammar, an order, limit and offset.
256
+ const filter = {
257
+ ...Object.fromEntries(Object.entries(q.filters).filter(([, values]) => values.length)),
258
+ customer: q.search ? { prefix: q.search } : undefined, // starts with, case ignored
259
+ };
260
+ const page = useRows({ source: "invoices", filter, orderBy: q.sort.map((s) => `${s.key} ${s.dir}`).join(", ") || undefined,
261
+ limit: q.pageSize, offset: (q.page - 1) * q.pageSize });
262
+ const total = useCount({ source: "invoices", filter });
263
+ const { status, ...others } = filter; // a facet counts over the other filters
264
+ const byStatus = useCountBy({ source: "invoices", column: "status", filter: others });
265
+ const statusCounts = Object.entries(byStatus.counts).map(([value, count]) => ({ value, label: value, count }));
266
+
267
+ <DataTable manual rows={page.rows} rowCount={total.count} loading={page.loading || total.loading}
257
268
  search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}
258
269
  filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}
259
270
  sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}
@@ -262,6 +273,12 @@ const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSiz
262
273
  columns={COLUMNS} facets={[{ key: "status", options: statusCounts }]} />
263
274
  ```
264
275
 
276
+ `useRows`, `useCount` and `useCountBy` are `lib/data.js`'s in a browser
277
+ app. `offset` goes up to 10,000; past it, page with `cursor` (the last
278
+ answer's `next`, given when `orderBy` names one column). A server app's
279
+ own `loadRows` takes no `offset`: its `"use client"` component takes
280
+ these hooks from `lib/door.js`.
281
+
265
282
  The pure helpers the table uses are exported too — `processRows`,
266
283
  `searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,
267
284
  `groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table
@@ -269,12 +286,25 @@ shows (a total over the filtered rows) without drawing it twice.
269
286
 
270
287
  ## Example
271
288
 
289
+ ```jsx
290
+ // surfaces/invoices.jsx — client: read, hand over
291
+ "use client";
292
+ import { useRows } from "../lib/data";
293
+ import Invoices from "../components/Invoices";
294
+
295
+ export default function InvoicesSurface() {
296
+ const { rows, error, loading } = useRows({ source: "invoices", orderBy: "due asc" });
297
+ if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table
298
+ return <Invoices invoices={rows} error={error} />;
299
+ }
300
+ ```
301
+
272
302
  ```jsx
273
303
  // components/Invoices.jsx — client: the table, the row sheet
274
304
  "use client";
275
305
  import { useState } from "react";
276
306
  import { updateRow } from "../lib/actions";
277
- import { Badge, Button, Sheet } from "./ui";
307
+ import { Badge, Button, Notice, Sheet } from "./ui";
278
308
  import { FixtureNotice, useFixture } from "./parts";
279
309
  import { DataTable } from "./blocks/data-table";
280
310
 
@@ -283,7 +313,7 @@ const COLUMNS = [
283
313
  { key: "number", header: "Invoice", sortable: true, hideable: false },
284
314
  { key: "customer", header: "Customer", sortable: true },
285
315
  { key: "status", header: "Status", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },
286
- { key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${r.amount.toLocaleString()}` },
316
+ { key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${Number(r.amount).toLocaleString()}` }, // numeric arrives as text
287
317
  { key: "due", header: "Due", sortable: true },
288
318
  ];
289
319
  const SAMPLE = [
@@ -291,14 +321,15 @@ const SAMPLE = [
291
321
  { number: "INV-1002", customer: "Acme Ltd", status: "overdue", amount: 450, due: "2026-09-01" },
292
322
  ];
293
323
 
294
- export default function Invoices({ invoices }) {
324
+ export default function Invoices({ invoices, error }) {
295
325
  const fixture = useFixture(invoices, SAMPLE);
296
326
  const [open, setOpen] = useState(null);
297
327
  const markPaid = (rows) =>
298
- Promise.all(rows.map((r) => updateRow({ source: "public.invoices", id: r.id, patch: { status: "paid" } })))
328
+ Promise.all(rows.map((r) => updateRow({ source: "invoices", id: r.id, patch: { status: "paid" } })))
299
329
  .then((all) => all.find((r) => r.ok === false) ?? { ok: true });
300
330
  return (
301
331
  <div className="grid gap-4">
332
+ {error ? <Notice tone="danger">{error}</Notice> : null}
302
333
  {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}
303
334
  <DataTable
304
335
  label="Invoices"
@@ -323,6 +354,8 @@ export default function Invoices({ invoices }) {
323
354
  }
324
355
  ```
325
356
 
357
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
358
+
326
359
  ### Details, groups and totals
327
360
 
328
361
  A budget: line items under departments, sorted within them, a subtotal
@@ -364,8 +397,8 @@ the row and the rest in the menu:
364
397
  ]} />
365
398
  ```
366
399
 
367
- `setStatus` and `remove` return a promise of `{ ok }`, as a Server Action
368
- does; a failure is said above the table.
400
+ `setStatus` and `remove` return a promise of `{ ok }`, as `updateRow` and
401
+ `deleteRow` do; a failure is said above the table.
369
402
 
370
403
  ## What it will not do
371
404
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  // Diagram — Mermaid text drawn in the workspace theme, with zoom and pan.
4
4
  //
5
- // Client only, one static bundle: Mermaid needs the DOM to measure text,
5
+ // Client only, one bundle: Mermaid needs the DOM to measure text,
6
6
  // and the app's check refuses import() (so no next/dynamic). The modules
7
7
  // load with the page; the drawing happens after the first effect (the
8
8
  // mount guard). Until then — on the server and in the first client paint —