makersclaw 0.5.2 → 0.7.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 (148) hide show
  1. package/README.md +73 -7
  2. package/package.json +3 -2
  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 +3 -3
  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 +3 -3
  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 +121 -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 +2 -2
  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 +14 -5
  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/chat/README.md +43 -13
  67. package/registry/blocks/code-editor/README.md +4 -0
  68. package/registry/blocks/code-editor/code-editor.jsx +1 -1
  69. package/registry/blocks/comment-box/README.md +20 -13
  70. package/registry/blocks/contact-section/README.md +7 -6
  71. package/registry/blocks/csv-import/README.md +34 -12
  72. package/registry/blocks/data-table/README.md +41 -8
  73. package/registry/blocks/diagram/diagram.jsx +1 -1
  74. package/registry/blocks/doc-editor/README.md +18 -6
  75. package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
  76. package/registry/blocks/doc-page/README.md +16 -6
  77. package/registry/blocks/file-upload/README.md +7 -1
  78. package/registry/blocks/flow-canvas/README.md +16 -5
  79. package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
  80. package/registry/blocks/footer/README.md +4 -2
  81. package/registry/blocks/gantt/README.md +18 -12
  82. package/registry/blocks/job-panel/README.md +1 -1
  83. package/registry/blocks/json-editor/README.md +16 -4
  84. package/registry/blocks/map/README.md +19 -3
  85. package/registry/blocks/map/map-view.jsx +1 -1
  86. package/registry/blocks/map/maplibre.js +1 -1
  87. package/registry/blocks/mind-map/README.md +16 -5
  88. package/registry/blocks/org-chart/README.md +10 -7
  89. package/registry/blocks/page-builder/README.md +13 -6
  90. package/registry/blocks/page-builder/page-builder.jsx +1 -1
  91. package/registry/blocks/page-builder/page-view.jsx +1 -1
  92. package/registry/blocks/page-builder/page.js +1 -1
  93. package/registry/blocks/pdf-viewer/README.md +22 -20
  94. package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
  95. package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
  96. package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
  97. package/registry/blocks/product-detail/README.md +13 -7
  98. package/registry/blocks/product-detail/demo.jsx +1 -1
  99. package/registry/blocks/product-grid/README.md +12 -10
  100. package/registry/blocks/record-form/README.md +26 -9
  101. package/registry/blocks/record-notes/README.md +42 -30
  102. package/registry/blocks/search-dialog/README.md +38 -7
  103. package/registry/blocks/settings-list/README.md +29 -17
  104. package/registry/blocks/setup-checklist/README.md +12 -14
  105. package/registry/blocks/share-dialog/README.md +1 -1
  106. package/registry/blocks/signature-pad/README.md +15 -4
  107. package/registry/blocks/sketch-canvas/README.md +13 -4
  108. package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
  109. package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
  110. package/registry/blocks/spreadsheet/README.md +17 -8
  111. package/registry/blocks/stat-cards/README.md +17 -8
  112. package/registry/blocks/task-list/README.md +14 -11
  113. package/registry/blocks/task-model/README.md +3 -3
  114. package/registry/blocks/transaction-list/README.md +15 -7
  115. package/registry/blocks/whiteboard/README.md +19 -13
  116. package/src/cli.mjs +65 -9
  117. package/src/install.mjs +360 -21
  118. package/src/lockfile.mjs +468 -0
  119. package/src/pm.mjs +473 -0
  120. package/src/registry.mjs +125 -1
  121. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  122. package/templates/next/browser/app/layout.jsx +94 -0
  123. package/templates/next/browser/app/page.jsx +60 -0
  124. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  125. package/templates/next/browser/lib/actions.js +16 -0
  126. package/templates/next/browser/lib/data.js +43 -0
  127. package/templates/next/files/README.md +818 -82
  128. package/templates/next/files/app/claw-alive/route.js +2 -2
  129. package/templates/next/files/app/frame.js +134 -9
  130. package/templates/next/files/app/layout.jsx +2 -2
  131. package/templates/next/files/check/run.mjs +1491 -53
  132. package/templates/next/files/components/ui/item.jsx +2 -2
  133. package/templates/next/files/components/ui/notice.jsx +1 -1
  134. package/templates/next/files/components/ui/separator.jsx +1 -1
  135. package/templates/next/files/lib/actions.js +171 -52
  136. package/templates/next/files/lib/brain-server.js +96 -0
  137. package/templates/next/files/lib/brain.js +413 -0
  138. package/templates/next/files/lib/claw.js +11 -0
  139. package/templates/next/files/lib/data.js +120 -30
  140. package/templates/next/files/lib/db.js +97 -17
  141. package/templates/next/files/lib/door.js +680 -0
  142. package/templates/next/files/next.config.mjs +85 -9
  143. package/templates/next/files/package.json +3 -3
  144. package/templates/next/files/{middleware.js → proxy.js} +13 -6
  145. package/templates/next/files/surfaces.js +3 -2
  146. package/templates/next/pnpm-lock.yaml +5870 -0
  147. package/templates/next/template.json +13 -6
  148. package/templates/next/files/schema.sql +0 -22
@@ -84,37 +84,37 @@ quantities by `key`.
84
84
  ## Example
85
85
 
86
86
  ```jsx
87
- // surfaces/shop.jsx — server: load, hand over
88
- import { loadRows } from "../lib/data";
87
+ // surfaces/shop.jsx — client: read, hand over
88
+ "use client";
89
+ import { useRows } from "../lib/data";
89
90
  import Shop from "../components/Shop";
90
91
 
91
- export default async function ShopSurface() {
92
- const { rows, error } = await loadRows({ source: "public.products", orderBy: "created_at desc" });
93
- return <Shop products={rows} error={error} />;
92
+ export default function ShopSurface() {
93
+ const { rows, error, loading, reload } = useRows({ source: "products", orderBy: "created_at desc" });
94
+ return <Shop products={rows} loading={loading} error={error} onRetry={reload} />;
94
95
  }
95
96
  ```
96
97
 
97
98
  ```jsx
98
99
  // components/Shop.jsx — client: the grid and the cart write
99
100
  "use client";
100
- import { useRouter } from "next/navigation";
101
101
  import { insertRow } from "../lib/actions";
102
102
  import { ProductGrid } from "./blocks/product-grid";
103
103
 
104
- export default function Shop({ products, error }) {
105
- const router = useRouter();
104
+ export default function Shop({ products, loading, error, onRetry }) {
106
105
  return (
107
106
  <ProductGrid
108
107
  products={products}
108
+ loading={loading}
109
109
  error={error}
110
- onRetry={() => router.refresh()}
110
+ onRetry={onRetry}
111
111
  filters={[
112
112
  { key: "category", label: "Category", options: ["Shirts", "Sweaters", "Jackets"] },
113
113
  { key: "size", label: "Size", options: ["S", "M", "L", "XL"] },
114
114
  ]}
115
115
  sort
116
116
  onAddToCart={(line) =>
117
- insertRow({ source: "public.cart_items", values: { product_id: line.id, line_key: line.key, quantity: 1 }, detail: line.name })
117
+ insertRow({ source: "cart_items", values: { product_id: line.id, line_key: line.key, quantity: 1 }, detail: line.name })
118
118
  }
119
119
  label="Shop"
120
120
  />
@@ -122,6 +122,8 @@ export default function Shop({ products, error }) {
122
122
  }
123
123
  ```
124
124
 
125
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
126
+
125
127
  Collection tiles, and a lead picture beside four products:
126
128
 
127
129
  ```jsx
@@ -93,7 +93,7 @@ Validation, first broken rule wins, one message per field:
93
93
  error?, errors? }` where `errors` is `{ fieldName: "message" }` — each
94
94
  shows under its field and the first one is focused. A throw counts as
95
95
  failed. In a Makersclaw app it is one line over `updateRow`:
96
- `onSave={(v) => updateRow({ source: "public.products", id: p.id, patch: v })}`.
96
+ `onSave={(v) => updateRow({ source: "products", id: p.id, patch: v })}`.
97
97
  - `container` — `"card"` (default: a Card with `title` and `description`)
98
98
  · `"page"` (no box: a page heading, then the sections) · `"dialog"` (a
99
99
  centred Sheet; on a phone a bottom sheet). A dialog needs `open` and
@@ -127,10 +127,12 @@ Validation, first broken rule wins, one message per field:
127
127
  ## Example
128
128
 
129
129
  ```jsx
130
- // components/ProductEditor.jsx — client
130
+ // surfaces/product.jsx — client: one product, edited
131
131
  "use client";
132
- import { updateRow, deleteRow } from "../lib/actions";
133
- import { RecordForm } from "./blocks/record-form";
132
+ import { useRouter, useSearchParams } from "next/navigation";
133
+ import { useRow } from "../lib/data";
134
+ import { deleteRow, updateRow } from "../lib/actions";
135
+ import { RecordForm } from "../components/blocks/record-form";
134
136
 
135
137
  const FIELDS = [
136
138
  { name: "name", label: "Product name", required: true },
@@ -140,20 +142,35 @@ const FIELDS = [
140
142
  { name: "details", label: "Details", type: "textarea" },
141
143
  ];
142
144
 
143
- export default function ProductEditor({ product }) {
145
+ export default function ProductEditor() {
146
+ const router = useRouter();
147
+ const id = useSearchParams().get("row");
148
+ const { row: product, loading, error, reload } = useRow({ source: "products", id });
144
149
  return (
145
150
  <RecordForm
146
151
  title="Edit product"
147
152
  fields={FIELDS}
148
- values={product}
149
- onSave={(values) => updateRow({ source: "public.products", id: product.id, patch: values })}
150
- onDelete={() => deleteRow({ source: "public.products", id: product.id })}
153
+ values={product ?? {}}
154
+ loading={loading}
155
+ error={error}
156
+ onRetry={reload}
157
+ onSave={(values) => updateRow({ source: "products", id, patch: values })}
158
+ onDelete={async () => {
159
+ const res = await deleteRow({ source: "products", id });
160
+ if (res.ok) router.push("/s/products");
161
+ return res;
162
+ }}
151
163
  deleteLabel="Remove product"
152
164
  />
153
165
  );
154
166
  }
155
167
  ```
156
168
 
169
+ After a save the row is read again and comes back as `values`: the
170
+ form starts over from what it just saved.
171
+
172
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
173
+
157
174
  In a dialog, from a list:
158
175
 
159
176
  ```jsx
@@ -170,7 +187,7 @@ const [editing, setEditing] = useState(null);
170
187
  { title: "Access", fields: [{ name: "role", label: "Role", type: "radio", options: ["Admin", "Member", "Viewer"] }] },
171
188
  ]}
172
189
  onSave={async (values) => {
173
- const res = await updateRow({ source: "public.members", id: editing.id, patch: values });
190
+ const res = await updateRow({ source: "members", id: editing.id, patch: values });
174
191
  return res.ok ? res : { ok: false, errors: res.error?.includes("email") ? { email: "Already in use." } : undefined, error: res.error };
175
192
  }}
176
193
  />
@@ -73,45 +73,57 @@ One table, a row per note: `record_id`, `author_id`, `body jsonb`,
73
73
  ## Example
74
74
 
75
75
  ```jsx
76
- // components/DealNotes.jsx — client
76
+ // surfaces/deal-notes.jsx — client: one deal's notes
77
77
  "use client";
78
- import { useState } from "react";
78
+ import { useSearchParams } from "next/navigation";
79
+ import { useArtifact } from "../lib/hooks";
80
+ import { useRows, whenLanded } from "../lib/data";
79
81
  import { deleteRow, insertRow, updateRow } from "../lib/actions";
80
- import { RecordNotes } from "./blocks/record-notes";
81
- import { docToText } from "./blocks/doc-editor";
82
+ import { RecordNotes } from "../components/blocks/record-notes";
83
+ import { docToText } from "../components/blocks/doc-editor";
82
84
 
83
- export default function DealNotes({ deal, me, team, initial }) {
84
- const [notes, setNotes] = useState(initial);
85
+ const source = "deal_notes";
86
+
87
+ // A write answers once the notes are read again, so the list never shows
88
+ // the notes from before it.
89
+ async function landed(res) {
90
+ if (res.ok) await whenLanded();
91
+ return res;
92
+ }
93
+
94
+ export default function DealNotes() {
95
+ const dealId = useSearchParams().get("row");
96
+ const { user } = useArtifact(); // the member signed in: { id, email }
97
+ const { rows, loading } = useRows({ source, filter: { deal_id: dealId }, orderBy: "created_at desc" });
98
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
99
+ const me = { id: user?.email, name: user?.email ?? "You" };
100
+ // created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
101
+ const notes = rows.map((n) => ({
102
+ id: n.id,
103
+ author: { id: n.created_by, name: n.created_by },
104
+ body: n.body,
105
+ createdAt: n.created_at,
106
+ updatedAt: n.updated_at,
107
+ }));
85
108
  const findPeople = async (q) =>
86
109
  team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
87
110
 
88
- async function add(doc) {
89
- const res = await insertRow({
90
- source: "public.deal_notes",
91
- values: { deal_id: deal.id, author_id: me.id, body: doc, body_text: docToText(doc) },
92
- });
93
- if (!res.ok) return res;
94
- setNotes((n) => [{ id: res.id, author: me, body: doc, createdAt: new Date() }, ...n]);
95
- return { ok: true };
96
- }
97
-
98
- async function edit(note, doc) {
99
- const updatedAt = new Date();
100
- const res = await updateRow({ source: "public.deal_notes", id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: updatedAt } });
101
- if (res.ok) setNotes((n) => n.map((x) => (x.id === note.id ? { ...x, body: doc, updatedAt } : x)));
102
- return res;
103
- }
104
-
105
- async function remove(note) {
106
- const res = await deleteRow({ source: "public.deal_notes", id: note.id });
107
- if (res.ok) setNotes((n) => n.filter((x) => x.id !== note.id));
108
- return res;
109
- }
110
-
111
- return <RecordNotes notes={notes} me={me} onAdd={add} onEdit={edit} onDelete={remove} mentions={findPeople} />;
111
+ if (loading) return null;
112
+ return (
113
+ <RecordNotes
114
+ notes={notes}
115
+ me={me}
116
+ onAdd={(doc) => insertRow({ source, values: { deal_id: dealId, body: doc, body_text: docToText(doc) } }).then(landed)}
117
+ onEdit={(note, doc) => updateRow({ source, id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: new Date() } }).then(landed)}
118
+ onDelete={(note) => deleteRow({ source, id: note.id }).then(landed)}
119
+ mentions={findPeople}
120
+ />
121
+ );
112
122
  }
113
123
  ```
114
124
 
125
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
126
+
115
127
  Beside the form, on a record page:
116
128
 
117
129
  ```jsx
@@ -70,15 +70,29 @@ Both components take these.
70
70
  ## Example
71
71
 
72
72
  ```jsx
73
+ // components/GlobalSearch.jsx — client: search as the member types
73
74
  "use client";
74
- import { useRouter } from "next/navigation";
75
75
  import { useState } from "react";
76
- import { Button } from "../components/ui";
77
- import { SearchDialog } from "../components/blocks/search-dialog";
76
+ import { useRouter } from "next/navigation";
77
+ import { loadRows, useRows } from "../lib/data";
78
+ import { Button } from "./ui";
79
+ import { SearchDialog } from "./blocks/search-dialog";
80
+
81
+ const asResult = (d) => ({ id: d.id, kind: "deal", title: d.name, snippet: d.stage });
78
82
 
79
- export function GlobalSearch({ recent, search }) {
83
+ export default function GlobalSearch() {
80
84
  const router = useRouter();
81
85
  const [open, setOpen] = useState(false);
86
+ const { rows: recent } = useRows({ source: "deals", orderBy: "updated_at desc", limit: 5 });
87
+
88
+ // The app's own table: names that start with what was typed, case ignored.
89
+ // The dialog aborts the older query through `signal`.
90
+ async function search(q, { signal }) {
91
+ const { rows, error } = await loadRows({ source: "deals", filter: { name: { prefix: q } }, orderBy: "name asc", limit: 20, signal });
92
+ if (error) throw new Error(error); // "Search failed"; an aborted read's answer is ignored
93
+ return rows.map(asResult);
94
+ }
95
+
82
96
  return (
83
97
  <>
84
98
  <Button icon="search" onClick={() => setOpen(true)}>Search</Button>
@@ -86,15 +100,32 @@ export function GlobalSearch({ recent, search }) {
86
100
  open={open}
87
101
  onOpenChange={setOpen}
88
102
  hotkey="k"
89
- sections={[{ heading: "Recent", results: recent }]}
90
- onSearch={(q) => search(q)} // a server action returning results
91
- onSelect={(r) => router.push(`/${r.kind}s/${r.id}`)}
103
+ sections={[{ heading: "Recent", results: recent.map(asResult) }]}
104
+ onSearch={search}
105
+ onSelect={(r) => router.push(`/s/deals?row=${r.id}`)}
92
106
  />
93
107
  </>
94
108
  );
95
109
  }
96
110
  ```
97
111
 
112
+ The company's records, through the brain (`lib/brain.js`), with
113
+ `meaning: false` while a person types: keywords only, nothing embedded
114
+ or charged. A search by meaning is charged per call; keep it for a
115
+ search page or an Enter.
116
+
117
+ ```jsx
118
+ import { searchBrain } from "../lib/brain";
119
+
120
+ async function search(q, { signal }) {
121
+ const { results, error } = await searchBrain({ q, meaning: false, limit: 20, signal });
122
+ if (error) throw new Error(error);
123
+ return results.map(({ record, snippet }) => ({ id: record.id, kind: record.kind, title: record.label, snippet }));
124
+ }
125
+ ```
126
+
127
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
128
+
98
129
  ## What it will not do
99
130
 
100
131
  - Index anything. It searches the list you give it, or asks your server.
@@ -71,7 +71,7 @@ The matrix (`variant="matrix"`) rows are events:
71
71
  false, error }`; a throw counts as failed. Without it, switches and
72
72
  selects are shown and cannot be changed. In a Makersclaw app it is one
73
73
  line over `saveSetting` (lib/actions.js):
74
- `onChange={(row, value) => saveSetting({ source: "public.settings", key: row.key, value })}`.
74
+ `onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}`.
75
75
  - `onAction(row, actionKey)` — run a button, a menu item or a remove.
76
76
  Return `{ ok }` like `onChange`. Remove the row from `rows` on success
77
77
  when it is gone (a signed-out device).
@@ -95,31 +95,43 @@ The matrix (`variant="matrix"`) rows are events:
95
95
  ## Example
96
96
 
97
97
  ```jsx
98
- // components/Preferences.jsx — client
98
+ // surfaces/preferences.jsx — client: settings, read and saved
99
99
  "use client";
100
+ import { useMemo } from "react";
101
+ import { useRows } from "../lib/data";
100
102
  import { saveSetting } from "../lib/actions";
101
- import { SettingsList } from "./blocks/settings-list";
102
-
103
- export default function Preferences({ settings }) {
104
- const rows = [
105
- { key: "digest", icon: "mail", label: "Weekly digest", description: "Every Monday at 8:00.", control: { type: "switch", value: settings.digest === true } },
106
- {
107
- key: "currency",
108
- icon: "dollar",
109
- label: "Currency",
110
- control: { type: "select", value: settings.currency ?? "usd", options: [{ value: "usd", label: "US dollar" }, { value: "eur", label: "Euro" }] },
111
- },
112
- ];
103
+ import { SettingsList } from "../components/blocks/settings-list";
104
+
105
+ export default function Preferences() {
106
+ const { rows: saved, loading, error, reload } = useRows({ source: "settings" });
107
+ // Built once per read: until `rows` changes, a row shows the value it saved.
108
+ const rows = useMemo(() => {
109
+ const settings = Object.fromEntries(saved.map((s) => [s.key, s.value]));
110
+ return [
111
+ { key: "digest", icon: "mail", label: "Weekly digest", description: "Every Monday at 8:00.", control: { type: "switch", value: settings.digest === true } },
112
+ {
113
+ key: "currency",
114
+ icon: "dollar",
115
+ label: "Currency",
116
+ control: { type: "select", value: settings.currency ?? "usd", options: [{ value: "usd", label: "US dollar" }, { value: "eur", label: "Euro" }] },
117
+ },
118
+ ];
119
+ }, [saved]);
113
120
  return (
114
121
  <SettingsList
115
122
  title="Preferences"
116
123
  rows={rows}
117
- onChange={(row, value) => saveSetting({ source: "public.settings", key: row.key, value })}
124
+ loading={loading}
125
+ error={error}
126
+ onRetry={reload}
127
+ onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}
118
128
  />
119
129
  );
120
130
  }
121
131
  ```
122
132
 
133
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
134
+
123
135
  Notifications per channel:
124
136
 
125
137
  ```jsx
@@ -131,7 +143,7 @@ Notifications per channel:
131
143
  { key: "mentions", group: "Conversations", label: "Mentions", values: { email: true, push: true } },
132
144
  { key: "signin", group: "Security", label: "New sign-in", values: { email: true, push: false }, locked: ["email"] },
133
145
  ]}
134
- onChange={(row, on, channel) => saveSetting({ mode: "json", source: "public.members", column: "notify", id: me.id, key: `${row.key}.${channel}`, value: on })}
146
+ onChange={(row, on, channel) => saveSetting({ mode: "json", source: "members", column: "notify", id: me.id, key: `${row.key}.${channel}`, value: on })}
135
147
  />
136
148
  ```
137
149
 
@@ -149,7 +161,7 @@ Signed-in devices, each with sign-out:
149
161
  meta: [{ icon: "globe", text: s.place }, { icon: "clock", text: s.lastSeen }],
150
162
  control: s.current ? null : { type: "remove", label: `Sign out ${s.device}`, confirm: { title: "Sign out this device?", confirmLabel: "Sign out" } },
151
163
  }))}
152
- onAction={(row) => deleteRow({ source: "public.sessions", id: row.key })}
164
+ onAction={(row) => deleteRow({ source: "sessions", id: row.key })}
153
165
  />
154
166
  ```
155
167
 
@@ -75,16 +75,15 @@ With no steps it says "Nothing to set up".
75
75
  ## Example
76
76
 
77
77
  ```jsx
78
- // surfaces/home.jsx — server: work out what is done
79
- import { loadRows } from "../lib/data";
78
+ // surfaces/home.jsx — client: work out what is done
79
+ "use client";
80
+ import { useCount } from "../lib/data";
80
81
  import GettingStarted from "../components/GettingStarted";
81
82
 
82
- export default async function Home() {
83
- const [{ rows: members }, { rows: deals }] = await Promise.all([
84
- loadRows({ source: "public.members" }),
85
- loadRows({ source: "public.deals", limit: 1 }),
86
- ]);
87
- return <GettingStarted invited={members.length > 1} hasDeal={deals.length > 0} />;
83
+ export default function Home() {
84
+ const members = useCount({ source: "members" });
85
+ const deals = useCount({ source: "deals" });
86
+ return <GettingStarted loading={members.loading || deals.loading} invited={members.count > 1} hasDeal={deals.count > 0} />;
88
87
  }
89
88
  ```
90
89
 
@@ -93,10 +92,11 @@ export default async function Home() {
93
92
  "use client";
94
93
  import { SetupChecklist } from "./blocks/setup-checklist";
95
94
 
96
- export default function GettingStarted({ invited, hasDeal, onHide }) {
95
+ export default function GettingStarted({ loading, invited, hasDeal, onHide }) {
97
96
  return (
98
97
  <SetupChecklist
99
98
  title="Get started"
99
+ loading={loading}
100
100
  steps={[
101
101
  { key: "invite", title: "Invite your team", description: "Bring in the people who own accounts.", actionLabel: "Invite", href: "/team", done: invited },
102
102
  { key: "deal", title: "Add your first deal", description: "One deal is enough to see the pipeline work.", actionLabel: "Add a deal", href: "/deals?new=1", done: hasDeal },
@@ -107,6 +107,8 @@ export default function GettingStarted({ invited, hasDeal, onHide }) {
107
107
  }
108
108
  ```
109
109
 
110
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
111
+
110
112
  In order, with buttons that do the work in place:
111
113
 
112
114
  ```jsx
@@ -115,11 +117,7 @@ In order, with buttons that do the work in place:
115
117
  sequential
116
118
  title="Quick start"
117
119
  steps={steps}
118
- onAction={async (step) => {
119
- const res = await runSetupStep(step.key); // your server action, { ok, error }
120
- if (res.ok) router.refresh(); // the step comes back done
121
- return res;
122
- }}
120
+ onAction={(step) => runAction({ name: "run_setup_step", args: { key: step.key } })} // an action named in references/actions.md; its ok re-reads every hook, so the step comes back done
123
121
  />
124
122
  ```
125
123
 
@@ -89,7 +89,7 @@ export function ShareProject({ project, members, invite, setRole, remove }) {
89
89
  onClose={() => setOpen(false)}
90
90
  title={`Share “${project.name}”`}
91
91
  people={members}
92
- onInvite={(emails, role) => invite(project.id, emails, role)} // server actions returning { ok, error }
92
+ onInvite={(emails, role) => invite(project.id, emails, role)} // each answers { ok } or { ok: false, error }, as lib/actions' writes do
93
93
  onRoleChange={(person, role) => setRole(project.id, person.id, role)}
94
94
  onRemove={(person) => remove(project.id, person.id)}
95
95
  link={`https://app.example.com/p/${project.id}`}
@@ -109,25 +109,36 @@ under the paper says so. Undo and clear are buttons with names.
109
109
  Accepting a quote in a Makersclaw app.
110
110
 
111
111
  ```jsx
112
+ // surfaces/quote.jsx — client: one quote, accepted and signed
112
113
  "use client";
114
+ import { useSearchParams } from "next/navigation";
115
+ import { useArtifact } from "../lib/hooks";
116
+ import { useRow } from "../lib/data";
113
117
  import { updateRow } from "../lib/actions";
114
- import { SignaturePad } from "./blocks/signature-pad";
118
+ import { SignaturePad } from "../components/blocks/signature-pad";
115
119
 
116
- export default function AcceptQuote({ quote, me }) {
120
+ export default function AcceptQuote() {
121
+ const id = useSearchParams().get("row");
122
+ const { user } = useArtifact(); // the member signed in: { id, email }
123
+ const { row: quote, loading } = useRow({ source: "quotes", id });
124
+ if (loading || !quote) return null;
117
125
  return (
118
126
  <SignaturePad
119
- signer={me.name}
127
+ signer={user?.email}
120
128
  value={quote.signature ?? undefined}
121
129
  submitLabel="Accept and sign"
122
130
  note="Signing records that you accept this quote. It is not a certified e-signature."
131
+ // signed_by: a before-update trigger sets it from the member, never the page (the app's README §5)
123
132
  onSubmit={(signature) =>
124
- updateRow({ source: "public.quotes", id: quote.id, patch: { status: "accepted", signature, signed_by: me.email }, detail: "accepted" })
133
+ updateRow({ source: "quotes", id: quote.id, patch: { status: "accepted", signature }, detail: "accepted" })
125
134
  }
126
135
  />
127
136
  );
128
137
  }
129
138
  ```
130
139
 
140
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
141
+
131
142
  ## What it will not do
132
143
 
133
144
  - Verify who signed, send the document for signature, or keep an audit
@@ -215,22 +215,31 @@ on a partial one:
215
215
  A whiteboard that saves itself:
216
216
 
217
217
  ```jsx
218
+ // surfaces/sketch.jsx — client: one board, saved as it is drawn
218
219
  "use client";
219
- import { SketchCanvas } from "./blocks/sketch-canvas";
220
+ import { useSearchParams } from "next/navigation";
221
+ import { useRow } from "../lib/data";
220
222
  import { updateRow } from "../lib/actions";
223
+ import { SketchCanvas } from "../components/blocks/sketch-canvas";
221
224
 
222
- export default function Board({ board }) {
225
+ export default function Board() {
226
+ const id = useSearchParams().get("row");
227
+ const { row: board, loading } = useRow({ source: "boards", id });
228
+ if (loading || !board) return null;
223
229
  return (
224
230
  <SketchCanvas
231
+ key={board.id}
225
232
  label={board.title}
226
- defaultValue={board.sketch} // jsonb, may be null
227
- onSave={(sketch) => updateRow({ source: "public.boards", id: board.id, patch: { sketch } })}
233
+ defaultValue={board.sketch} // jsonb, may be null; read once per board
234
+ onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
228
235
  className="h-dvh rounded-none border-0"
229
236
  />
230
237
  );
231
238
  }
232
239
  ```
233
240
 
241
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
242
+
234
243
  Marking up a screenshot, with numbered notes:
235
244
 
236
245
  ```jsx
@@ -2,7 +2,7 @@
2
2
 
3
3
  // SketchCanvas — Excalidraw, in the workspace theme, behind one set of props.
4
4
  //
5
- // Client only, one static bundle. Excalidraw needs the DOM, and the app's
5
+ // Client only, one bundle. Excalidraw needs the DOM, and the app's
6
6
  // check refuses import() (so no next/dynamic): the modules load with the
7
7
  // page (on the server too — see excalidraw.js and ssr-shim.js for how that
8
8
  // is made safe), and the canvas itself mounts only after the first effect
@@ -5,9 +5,9 @@
5
5
  // devicePixelRatio, document.createElement("img"), an Element.prototype
6
6
  // polyfill), so evaluating it on the server throws "window is not defined"
7
7
  // and the page fails to render — even though the component itself only
8
- // ever renders in the browser (the mount guard). The app is one static
9
- // bundle (no import(), no next/dynamic), so the module cannot be kept off
10
- // the server; instead it is let load against these stand-ins.
8
+ // ever renders in the browser (the mount guard). The app is one bundle
9
+ // (no import(), no next/dynamic), so the module cannot be kept off the
10
+ // server; instead it is let load against these stand-ins.
11
11
  //
12
12
  // The stand-ins live only for that load: excalidraw.js imports this file,
13
13
  // then @excalidraw/excalidraw, then ssr-shim-end.js, which removes them —
@@ -241,10 +241,12 @@ Shift+F10 the row menu.
241
241
  ## Example
242
242
 
243
243
  ```jsx
244
+ // surfaces/deals.jsx — client: the deals as a grid
244
245
  "use client";
245
246
  import { useState } from "react";
246
- import { updateRow, insertRow, deleteRow } from "../lib/actions";
247
- import { Spreadsheet } from "./blocks/spreadsheet";
247
+ import { useRows } from "../lib/data";
248
+ import { deleteRow, insertRow, updateRow } from "../lib/actions";
249
+ import { Spreadsheet } from "../components/blocks/spreadsheet";
248
250
 
249
251
  const COLUMNS = [
250
252
  { key: "name", name: "Name", type: "text", required: true },
@@ -253,20 +255,25 @@ const COLUMNS = [
253
255
  { key: "close", name: "Close date", type: "date" },
254
256
  ];
255
257
 
256
- export default function Deals({ deals, team }) {
258
+ export default function Deals() {
259
+ const deals = useRows({ source: "deals", orderBy: "created_at asc" });
260
+ const team = useRows({ source: "people", orderBy: "name asc" });
257
261
  const [view, setView] = useState({ sort: [{ key: "amount", dir: "desc" }] });
262
+ // Each change below is one write, and the door takes 300 a minute: a
263
+ // paste larger than that belongs in one action (runAction).
258
264
  return (
259
265
  <Spreadsheet
260
266
  label="Deals"
261
267
  noun="deal"
262
- rows={deals}
268
+ rows={deals.rows}
269
+ loading={deals.loading}
263
270
  columns={COLUMNS}
264
- people={team}
271
+ people={team.rows}
265
272
  view={view}
266
273
  onViewChange={setView}
267
274
  onChange={async (changes) => {
268
275
  for (const c of changes) {
269
- const r = await updateRow({ source: "public.deals", id: c.rowId, patch: { [c.key]: c.value } });
276
+ const r = await updateRow({ source: "deals", id: c.rowId, patch: { [c.key]: c.value } });
270
277
  if (!r.ok) return r;
271
278
  }
272
279
  return { ok: true };
@@ -274,7 +281,7 @@ export default function Deals({ deals, team }) {
274
281
  onAddRows={async (rows) => {
275
282
  const made = [];
276
283
  for (const { id, ...values } of rows) {
277
- const r = await insertRow({ source: "public.deals", values: { stage: "lead", ...values } });
284
+ const r = await insertRow({ source: "deals", values: { stage: "lead", ...values } });
278
285
  if (!r.ok) return r;
279
286
  made.push({ id: r.id }); // the database's id replaces the one the grid made up
280
287
  }
@@ -282,7 +289,7 @@ export default function Deals({ deals, team }) {
282
289
  }}
283
290
  onDeleteRows={async (ids) => {
284
291
  for (const id of ids) {
285
- const r = await deleteRow({ source: "public.deals", id });
292
+ const r = await deleteRow({ source: "deals", id });
286
293
  if (!r.ok) return r;
287
294
  }
288
295
  return { ok: true };
@@ -292,6 +299,8 @@ export default function Deals({ deals, team }) {
292
299
  }
293
300
  ```
294
301
 
302
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
303
+
295
304
  ## What it will not do
296
305
 
297
306
  - No formulas. A value is what was typed; a derived column is computed by