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
@@ -57,8 +57,11 @@ true) is where it starts. The icon rail shows every group unfolded.
57
57
  - `shortcut` — Cmd/Ctrl + this key toggles it; default `"b"`, `null` for none.
58
58
  - `variant` — `"inset"` draws the page as a card on the frame's ground.
59
59
  Default: the Sidebar's own `variant`, so you rarely pass it here.
60
- - `className` — the frame's height and edges (`min-h-dvh` in a shell).
61
- Never colour.
60
+ - `className` — the frame's edges, and its height from its parent: in a
61
+ shell it sits in the kit's `AppLayout contentMode="fill"` with
62
+ `min-h-0 flex-1` (the Example). The kit's `AppLayout` owns the
63
+ window's height; `min-h-dvh` or `h-dvh` here is refused by `npm run
64
+ check`. Never colour.
62
65
 
63
66
  ### Sidebar
64
67
 
@@ -138,6 +141,7 @@ The header button: opens the sheet on a phone, collapses to icons from
138
141
  // app/shell.jsx — the app's shell, the surfaces as the sidebar's items
139
142
  "use client";
140
143
  import { usePathname } from "next/navigation";
144
+ import { AppLayout } from "../components/ui";
141
145
  import { Sidebar, SidebarLayout, SidebarTrigger } from "../components/blocks/sidebar";
142
146
 
143
147
  const ICONS = { inbox: "inbox", deals: "dollar", contacts: "users", reports: "chart" };
@@ -153,18 +157,24 @@ export default function Shell({ name, surfaces, children }) {
153
157
  active: s.key === activeKey,
154
158
  badge: s.built ? undefined : "planned",
155
159
  }));
160
+ // The outer AppLayout owns the window; the inner one (it fills its
161
+ // slot) is the content that scrolls, back to its top on a new route.
156
162
  return (
157
- <SidebarLayout
158
- storageKey={`${name}.sidebar`}
159
- className="min-h-dvh"
160
- sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}
161
- >
162
- <header className="flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden">
163
- <SidebarTrigger />
164
- <span className="font-semibold">{name}</span>
165
- </header>
166
- <main className="min-w-0 flex-1 p-4 sm:p-6">{children}</main>
167
- </SidebarLayout>
163
+ <AppLayout contentMode="fill">
164
+ <SidebarLayout
165
+ storageKey={`${name}.sidebar`}
166
+ className="min-h-0 flex-1"
167
+ sidebar={<Sidebar title={name} items={items} label={`${name} surfaces`} />}
168
+ >
169
+ <header className="flex min-h-14 items-center gap-2 border-b border-border px-2 md:hidden">
170
+ <SidebarTrigger />
171
+ <span className="font-semibold">{name}</span>
172
+ </header>
173
+ <AppLayout contentClassName="p-4 sm:p-6" scrollKey={pathname}>
174
+ {children}
175
+ </AppLayout>
176
+ </SidebarLayout>
177
+ </AppLayout>
168
178
  );
169
179
  }
170
180
  ```
@@ -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
@@ -130,8 +130,11 @@ A sketch is plain JSON. Store it as it comes out of `onChange` / `onSave`
130
130
  - `onReady(handle)` — once, when the canvas has mounted.
131
131
  - `label` — the accessible name of the canvas region (default "Sketch").
132
132
  - `className` — layout: width, margin, height. The default height is
133
- `h-[32rem]`; a height class of yours (`h-dvh`, `h-full`, `min-h-…`,
134
- `flex-1`) replaces it. Never colour.
133
+ `h-[32rem]`; a height class of yours (`h-full`, `min-h-…`, `min-h-0
134
+ flex-1`) replaces it. A canvas that is the whole surface sits in an
135
+ `AppLayout contentMode="fill"` from the kit with `min-h-0 flex-1` (the
136
+ Example): never `h-dvh` or a `vh` height, which `npm run check`
137
+ refuses. Never colour.
135
138
 
136
139
  ### The handle
137
140
 
@@ -215,22 +218,34 @@ on a partial one:
215
218
  A whiteboard that saves itself:
216
219
 
217
220
  ```jsx
221
+ // surfaces/sketch.jsx — client: one board, saved as it is drawn
218
222
  "use client";
219
- import { SketchCanvas } from "./blocks/sketch-canvas";
223
+ import { useSearchParams } from "next/navigation";
224
+ import { useRow } from "../lib/data";
220
225
  import { updateRow } from "../lib/actions";
226
+ import { AppLayout } from "../components/ui";
227
+ import { SketchCanvas } from "../components/blocks/sketch-canvas";
221
228
 
222
- export default function Board({ board }) {
229
+ export default function Board() {
230
+ const id = useSearchParams().get("row");
231
+ const { row: board, loading } = useRow({ source: "boards", id });
232
+ if (loading || !board) return null;
223
233
  return (
224
- <SketchCanvas
225
- label={board.title}
226
- defaultValue={board.sketch} // jsonb, may be null
227
- onSave={(sketch) => updateRow({ source: "public.boards", id: board.id, patch: { sketch } })}
228
- className="h-dvh rounded-none border-0"
229
- />
234
+ <AppLayout contentMode="fill">
235
+ <SketchCanvas
236
+ key={board.id}
237
+ label={board.title}
238
+ defaultValue={board.sketch} // jsonb, may be null; read once per board
239
+ onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch } })}
240
+ className="min-h-0 flex-1 rounded-none border-0"
241
+ />
242
+ </AppLayout>
230
243
  );
231
244
  }
232
245
  ```
233
246
 
247
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
248
+
234
249
  Marking up a screenshot, with numbered notes:
235
250
 
236
251
  ```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
@@ -125,27 +125,34 @@ States
125
125
  ## Example
126
126
 
127
127
  ```jsx
128
- // components/Overview.jsx — client: the numbers the server read
128
+ // surfaces/overview.jsx — client: the numbers, read in the browser
129
129
  "use client";
130
- import { StatCards } from "./blocks/stat-cards";
130
+ import { useRows } from "../lib/data";
131
131
  import { updateRow } from "../lib/actions";
132
+ import { StatCards } from "../components/blocks/stat-cards";
132
133
 
133
- export default function Overview({ kpis, usage }) {
134
+ export default function Overview() {
135
+ const summary = useRows({ source: "kpi_summary", limit: 1 }); // a view of the app's: this week's numbers, one row
136
+ const usage = useRows({ source: "quotas", orderBy: "name asc" });
137
+ const kpis = summary.rows[0] ?? {};
134
138
  return (
135
139
  <div className="grid gap-6">
136
140
  <StatCards
141
+ loading={summary.loading}
142
+ error={summary.error}
137
143
  items={[
138
- { id: "mrr", label: "MRR", value: kpis.mrr, delta: kpis.mrrChange, trend: kpis.mrrWeeks, href: "/revenue" },
139
- { id: "churn", label: "Logo churn", value: kpis.churn, delta: kpis.churnChange, invert: true },
140
- { id: "trials", label: "Active trials", value: kpis.trials, previous: kpis.trialsLastWeek, delta: kpis.trialsChange },
144
+ { id: "mrr", label: "MRR", value: kpis.mrr, delta: kpis.mrr_change, trend: kpis.mrr_weeks, href: "/s/revenue" },
145
+ { id: "churn", label: "Logo churn", value: kpis.churn, delta: kpis.churn_change, invert: true },
146
+ { id: "trials", label: "Active trials", value: kpis.trials, previous: kpis.trials_last_week, delta: kpis.trials_change },
141
147
  ]}
142
148
  variant="cards"
143
149
  />
144
150
  <StatCards
145
151
  variant="usage"
146
152
  title="Plan usage"
147
- items={usage.map((u) => ({ id: u.id, label: u.name, used: u.used, limit: u.limit, unit: u.unit }))}
148
- onLimitChange={(item, limit) => updateRow({ source: "public.quotas", id: item.id, patch: { limit } })}
153
+ loading={usage.loading}
154
+ items={usage.rows.map((u) => ({ id: u.id, label: u.name, used: u.used, limit: u.limit, unit: u.unit }))}
155
+ onLimitChange={(item, limit) => updateRow({ source: "quotas", id: item.id, patch: { limit } })}
149
156
  limitLabel="Change budget"
150
157
  />
151
158
  </div>
@@ -153,6 +160,8 @@ export default function Overview({ kpis, usage }) {
153
160
  }
154
161
  ```
155
162
 
163
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
164
+
156
165
  ## What it will not do
157
166
 
158
167
  - Fetch or compute: numbers come in formatted, changes already worked out.
@@ -98,14 +98,15 @@ drag-to-reorder saves; tasks without one keep the order given.
98
98
  ## Example
99
99
 
100
100
  ```jsx
101
- // surfaces/tasks.jsx — server: load, hand over
102
- import { loadRows } from "../lib/data";
101
+ // surfaces/tasks.jsx — client: read, hand over
102
+ "use client";
103
+ import { useRows } from "../lib/data";
103
104
  import Tasks from "../components/Tasks";
104
105
 
105
- export default async function TasksSurface() {
106
- const { rows, error } = await loadRows({ source: "public.tasks", orderBy: "created_at asc" });
107
- const people = await loadRows({ source: "public.people" });
108
- return <Tasks tasks={rows} people={people.rows} error={error} />;
106
+ export default function TasksSurface() {
107
+ const tasks = useRows({ source: "tasks", orderBy: "created_at asc" });
108
+ const people = useRows({ source: "people" });
109
+ return <Tasks tasks={tasks.rows} people={people.rows} loading={tasks.loading || people.loading} error={tasks.error} />;
109
110
  }
110
111
  ```
111
112
 
@@ -132,19 +133,19 @@ const COLUMNS = ["id", "title", "status", "assignees", "due", "priority", "tags"
132
133
  async function save(change) {
133
134
  for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
134
135
  const r =
135
- w.op === "insert" ? await insertRow({ source: "public.tasks", values: w.values, detail: w.values.title })
136
- : w.op === "update" ? await updateRow({ source: "public.tasks", id: w.id, patch: w.patch })
137
- : await deleteRow({ source: "public.tasks", id: w.id });
136
+ w.op === "insert" ? await insertRow({ source: "tasks", values: w.values, detail: w.values.title })
137
+ : w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
138
+ : await deleteRow({ source: "tasks", id: w.id });
138
139
  if (r?.ok === false) return r;
139
140
  }
140
141
  return { ok: true };
141
142
  }
142
143
 
143
- export default function Tasks({ tasks, people, error }) {
144
+ export default function Tasks({ tasks, people, loading, error }) {
144
145
  const [open, setOpen] = useState(null);
145
146
  return (
146
147
  <>
147
- <TaskList tasks={tasks} statuses={STATUSES} people={people} onChange={save} onOpen={setOpen} error={error}
148
+ <TaskList tasks={tasks} statuses={STATUSES} people={people} onChange={save} onOpen={setOpen} loading={loading} error={error}
148
149
  starters={[{ label: "Weekly planning", tasks: [{ title: "Review last week" }, { title: "Pick three priorities" }] }]} />
149
150
  <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>
150
151
  </>
@@ -152,6 +153,8 @@ export default function Tasks({ tasks, people, error }) {
152
153
  }
153
154
  ```
154
155
 
156
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
157
+
155
158
  Grouped by person, only open work, showing the project:
156
159
 
157
160
  ```jsx
@@ -113,9 +113,9 @@ const COLUMNS = ["id", "title", "status", "assignees", "due", "priority", "paren
113
113
 
114
114
  async function onChange(change) {
115
115
  for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
116
- const r = w.op === "insert" ? await insertRow({ source: "public.tasks", values: w.values })
117
- : w.op === "update" ? await updateRow({ source: "public.tasks", id: w.id, patch: w.patch })
118
- : await deleteRow({ source: "public.tasks", id: w.id });
116
+ const r = w.op === "insert" ? await insertRow({ source: "tasks", values: w.values })
117
+ : w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
118
+ : await deleteRow({ source: "tasks", id: w.id });
119
119
  if (r.ok === false) return r;
120
120
  }
121
121
  return { ok: true };
@@ -79,22 +79,28 @@ An invoice row:
79
79
  ## Example
80
80
 
81
81
  ```jsx
82
- // components/Billing.jsx — client
82
+ // surfaces/billing.jsx — client: invoices and payments
83
83
  "use client";
84
- import { TransactionList } from "./blocks/transaction-list";
84
+ import { useRows } from "../lib/data";
85
+ import { TransactionList } from "../components/blocks/transaction-list";
85
86
 
86
- export default function Billing({ invoices, payments, error }) {
87
+ export default function Billing() {
88
+ const invoices = useRows({ source: "invoices", orderBy: "date desc" });
89
+ const payments = useRows({ source: "payments", orderBy: "date desc" });
87
90
  return (
88
91
  <div className="grid gap-6 lg:grid-cols-2">
89
92
  <TransactionList
90
93
  variant="invoices"
91
- rows={invoices}
92
- error={error}
94
+ rows={invoices.rows}
95
+ loading={invoices.loading}
96
+ error={invoices.error}
93
97
  description="Every invoice we have sent you."
94
- downloadHref={(inv) => inv.pdfUrl}
98
+ downloadHref={(inv) => inv.pdf_url}
95
99
  />
96
100
  <TransactionList
97
- rows={payments}
101
+ rows={payments.rows}
102
+ loading={payments.loading}
103
+ error={payments.error}
98
104
  dateFilter
99
105
  description="Money in and out of the account."
100
106
  onExport={async (rows) => {
@@ -109,6 +115,8 @@ export default function Billing({ invoices, payments, error }) {
109
115
  }
110
116
  ```
111
117
 
118
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
119
+
112
120
  ## What it will not do
113
121
 
114
122
  - Sort, page, or filter by column: rows come in ordered, and a record set
@@ -93,8 +93,11 @@ for agents.
93
93
  receives the canvas handle: `exportImage()`, `getScene()`,
94
94
  `insertElements()`… (sketch-canvas README, "The handle").
95
95
  - `className` — layout: width, margin, height. The default height is
96
- `h-[40rem]`; a height class of yours (`h-dvh` for a full page, `h-full`,
97
- `flex-1`) replaces it. Never colour.
96
+ `h-[40rem]`; a height class of yours (`h-full`, `min-h-0 flex-1`)
97
+ replaces it. A board that is the whole surface sits in an `AppLayout
98
+ contentMode="fill"` from the kit with `min-h-0 flex-1` (the Example):
99
+ never `h-dvh` or a `vh` height, which `npm run check` refuses. Never
100
+ colour.
98
101
 
99
102
  ### Templates
100
103
 
@@ -113,39 +116,48 @@ reuse them:
113
116
  A board page that saves itself:
114
117
 
115
118
  ```jsx
119
+ // surfaces/board.jsx — client: one board, saved as it changes
116
120
  "use client";
117
- import { useState } from "react";
118
- import { Whiteboard } from "./blocks/whiteboard";
119
- import { sketchToText } from "./blocks/sketch-canvas";
121
+ import { useSearchParams } from "next/navigation";
122
+ import { useRow, whenLanded } from "../lib/data";
120
123
  import { updateRow } from "../lib/actions";
121
-
122
- export default function BoardPage({ board, onShare }) {
123
- const [title, setTitle] = useState(board.title);
124
+ import { AppLayout } from "../components/ui";
125
+ import { Whiteboard } from "../components/blocks/whiteboard";
126
+ import { sketchToText } from "../components/blocks/sketch-canvas";
127
+
128
+ export default function BoardPage() {
129
+ const id = useSearchParams().get("row");
130
+ const { row: board, loading } = useRow({ source: "boards", id });
131
+ if (loading || !board) return null;
124
132
  return (
125
- <Whiteboard
126
- title={title}
127
- onRename={async (next) => {
128
- const res = await updateRow({ source: "public.boards", id: board.id, patch: { title: next } });
129
- if (res.ok) setTitle(next);
130
- return res;
131
- }}
132
- defaultValue={board.sketch} // jsonb, may be null
133
- onSave={(sketch) => updateRow({ source: "public.boards", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}
134
- onShare={onShare}
135
- className="h-dvh rounded-none border-0"
136
- />
133
+ <AppLayout contentMode="fill">
134
+ <Whiteboard
135
+ key={board.id}
136
+ title={board.title}
137
+ onRename={async (next) => {
138
+ const res = await updateRow({ source: "boards", id: board.id, patch: { title: next } });
139
+ if (res.ok) await whenLanded(); // the board is read again: the title is the new one
140
+ return res;
141
+ }}
142
+ defaultValue={board.sketch} // jsonb, may be null; read once per board
143
+ onSave={(sketch) => updateRow({ source: "boards", id: board.id, patch: { sketch, sketch_text: sketchToText(sketch) } })}
144
+ className="min-h-0 flex-1 rounded-none border-0"
145
+ />
146
+ </AppLayout>
137
147
  );
138
148
  }
139
149
  ```
140
150
 
151
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
152
+
141
153
  Notes that become tasks (a `tasks` table of your own):
142
154
 
143
155
  ```jsx
144
156
  <Whiteboard
145
157
  defaultValue={board.sketch}
146
158
  onSave={save}
147
- onCreateTask={({ title, sourceIds }) => insertRow({ source: "public.tasks", values: { title, board_id: board.id, source_ids: sourceIds } })}
148
- onOpenTask={(task) => router.push(`/tasks/${task.id}`)}
159
+ onCreateTask={({ title, sourceIds }) => insertRow({ source: "tasks", values: { title, board_id: board.id, source_ids: sourceIds } })}
160
+ onOpenTask={(task) => router.push(`/s/tasks?row=${task.id}`)}
149
161
  />
150
162
  ```
151
163
 
package/src/cli.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // --json is the same result as one object, for the platform.
5
5
 
6
6
  import { delimiter, resolve } from "node:path";
7
- import { CliError, add, diff, init, install, remove, update } from "./install.mjs";
7
+ import { CliError, add, diff, init, install, mode, remove, update } from "./install.mjs";
8
8
  import { loadItems, loadTemplate, packageInfo, readmeOf, searchItems, suggest, templateNames, useExtraBlockRoots } from "./registry.mjs";
9
9
 
10
10
  const COMMANDS = {
@@ -23,6 +23,7 @@ const COMMANDS = {
23
23
  upgrade: "update",
24
24
  install: "install",
25
25
  i: "install",
26
+ mode: "mode",
26
27
  help: "help",
27
28
  };
28
29
 
@@ -32,6 +33,8 @@ const VALUE_FLAGS = new Map([
32
33
  ["--template", "template"],
33
34
  ["--type", "type"],
34
35
  ["--limit", "limit"],
36
+ ["--tables", "tables"],
37
+ ["--mode", "mode"],
35
38
  ]);
36
39
  const BOOL_FLAGS = new Map([
37
40
  ["--json", "json"],
@@ -76,6 +79,8 @@ function helpText() {
76
79
  return `makersclaw ${version} — the Makersclaw app template, its kit and its blocks
77
80
 
78
81
  npx makersclaw init [dir] [-t next] lay the template: base files, the kit, the check
82
+ [--tables own|public] where its tables live (new apps: own; never changes an app's)
83
+ [--mode browser|server] its mode (new apps: server; browser writes tables own too)
79
84
  npx makersclaw add <name...> install blocks, or restore a kit component
80
85
  npx makersclaw search <words...> find blocks and components by what they do
81
86
  npx makersclaw view <name> one item: props, data, an example
@@ -85,6 +90,7 @@ function helpText() {
85
90
  npx makersclaw update [--major] lay this version's fixed files and blocks
86
91
  [--adopt] …or take on an app laid before makersclaw (kit era)
87
92
  npx makersclaw install the app's packages: pnpm, from pnpm-lock.yaml
93
+ npx makersclaw mode <browser|server> switch the app's mode: its fixed files, and what its code must change
88
94
 
89
95
  --cwd <dir> the app (default: this folder) --json one JSON object
90
96
  --force init over an app that exists (your own files stay)
@@ -101,7 +107,7 @@ const listLine = (paths, n = 6) => (paths.length > n ? `${paths.slice(0, n).join
101
107
 
102
108
  function packageLines(changes) {
103
109
  if (!changes.length) return [];
104
- return [` package.json: ${changes.map((c) => `${c.name} ${c.from ?? "(new)"} → ${c.to}`).join(" · ")}`];
110
+ return [` package.json: ${changes.map((c) => `${c.name} ${c.from ?? "(new)"} → ${c.to ?? "(removed)"}`).join(" · ")}`];
105
111
  }
106
112
 
107
113
  /** The lock's line, when it was written. What it left out is a note. */
@@ -127,7 +133,7 @@ function layText(r, verb) {
127
133
  if (r.blocks.length) lines.push(` blocks: ${r.blocks.join(", ")}`);
128
134
  lines.push(...packageLines(r.package));
129
135
  lines.push(...lockLine(r.lock, r.written.includes("pnpm-lock.yaml")));
130
- lines.push(` ${plural(r.pinned, "fixed file")} pinned in check/pins.json · makersclaw.json records ${r.version}`);
136
+ lines.push(` ${plural(r.pinned, "fixed file")} pinned in check/pins.json · makersclaw.json records ${r.version}, tables ${r.tables}, mode ${r.mode}`);
131
137
  for (const n of r.notes) lines.push(` note: ${n}`);
132
138
  const next = [];
133
139
  if (r.install) next.push("npx makersclaw install");
@@ -200,14 +206,31 @@ function listText(items) {
200
206
  return lines.join("\n");
201
207
  }
202
208
 
209
+ function modeText(r) {
210
+ const lines = [
211
+ r.changed
212
+ ? `makersclaw ${r.version} · template ${r.template} · mode ${r.was} → ${r.mode}`
213
+ : `makersclaw ${r.version} · template ${r.template} · already ${r.mode}: its fixed files laid again`,
214
+ ];
215
+ if (r.written.length) lines.push(` wrote ${plural(r.written.length, "file")}: ${listLine(r.written)}`);
216
+ if (r.removed.length) lines.push(` removed (${r.mode} mode lays none): ${listLine(r.removed)}`);
217
+ lines.push(...packageLines(r.package));
218
+ lines.push(` ${plural(r.pinned, "fixed file")} pinned in check/pins.json · makersclaw.json records mode ${r.mode}`);
219
+ for (const n of r.notes) lines.push(` note: ${n}`);
220
+ lines.push(`the app's own code${r.changed ? " must change" : ", in this mode"}:`);
221
+ for (const c of r.change) lines.push(` - ${c}`);
222
+ lines.push(`next: ${[...(r.install ? ["npx makersclaw install"] : []), "npm run check"].join(" · ")}`);
223
+ return lines.join("\n");
224
+ }
225
+
203
226
  function diffText(r) {
204
- const lines = [`makersclaw ${r.from} → ${r.to} · ${r.compatible ? "compatible" : "changes the contract — update --major"}`];
227
+ const lines = [`makersclaw ${r.from} → ${r.to} · ${r.compatible ? "compatible" : "changes the contract — update --major"} · mode ${r.mode}`];
205
228
  if (!r.entries.length && !r.package.length) {
206
229
  lines.push(` nothing to update — the app matches makersclaw ${r.to}`);
207
230
  return lines.join("\n");
208
231
  }
209
232
  for (const e of r.entries) lines.push(` ${e.item.padEnd(14)} ${e.status.padEnd(8)} ${e.path}${e.detail ? ` — ${e.detail}` : ""}`);
210
- for (const c of r.package) lines.push(` ${"package.json".padEnd(14)} ${c.key}.${c.name}: ${c.from ?? "(new)"} → ${c.to}`);
233
+ for (const c of r.package) lines.push(` ${"package.json".padEnd(14)} ${c.key}.${c.name}: ${c.from ?? "(new)"} → ${c.to ?? "(removed)"}`);
211
234
  lines.push(`next: npx makersclaw update${r.compatible ? "" : " --major"}`);
212
235
  return lines.join("\n");
213
236
  }
@@ -240,7 +263,7 @@ export function run(opts, { stderr = process.stderr, env = process.env } = {}) {
240
263
  return { result: packageInfo(), text: packageInfo().version };
241
264
  case "init": {
242
265
  const dir = opts.args[0] ? resolve(cwd, opts.args[0]) : cwd;
243
- const result = init(dir, { template: opts.template ?? "next", force: Boolean(opts.force) });
266
+ const result = init(dir, { template: opts.template ?? "next", force: Boolean(opts.force), tables: opts.tables, mode: opts.mode });
244
267
  return { result: { dir, ...result }, text: layText(result, "init") };
245
268
  }
246
269
  case "update": {
@@ -262,6 +285,10 @@ export function run(opts, { stderr = process.stderr, env = process.env } = {}) {
262
285
  const result = install(cwd, { env, onNote: (line) => stderr.write(`makersclaw: ${line}\n`) });
263
286
  return { result: { dir: cwd, ...result }, text: installText(result) };
264
287
  }
288
+ case "mode": {
289
+ const result = mode(cwd, opts.args[0] ?? opts.mode);
290
+ return { result: { dir: cwd, ...result }, text: modeText(result) };
291
+ }
265
292
  case "diff": {
266
293
  const result = diff(cwd);
267
294
  return { result: { dir: cwd, ...result }, text: diffText(result) };