makersclaw 0.6.0 → 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 (141) hide show
  1. package/README.md +48 -8
  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 +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 +31 -4
  117. package/src/install.mjs +263 -21
  118. package/src/registry.mjs +78 -1
  119. package/templates/next/browser/app/auto-refresh.jsx +67 -0
  120. package/templates/next/browser/app/layout.jsx +94 -0
  121. package/templates/next/browser/app/page.jsx +60 -0
  122. package/templates/next/browser/app/s/[key]/page.jsx +95 -0
  123. package/templates/next/browser/lib/actions.js +16 -0
  124. package/templates/next/browser/lib/data.js +43 -0
  125. package/templates/next/files/README.md +810 -79
  126. package/templates/next/files/app/frame.js +128 -4
  127. package/templates/next/files/check/run.mjs +1438 -41
  128. package/templates/next/files/components/ui/item.jsx +2 -2
  129. package/templates/next/files/components/ui/notice.jsx +1 -1
  130. package/templates/next/files/components/ui/separator.jsx +1 -1
  131. package/templates/next/files/lib/actions.js +170 -51
  132. package/templates/next/files/lib/brain-server.js +96 -0
  133. package/templates/next/files/lib/brain.js +413 -0
  134. package/templates/next/files/lib/claw.js +11 -0
  135. package/templates/next/files/lib/data.js +119 -29
  136. package/templates/next/files/lib/db.js +97 -17
  137. package/templates/next/files/lib/door.js +680 -0
  138. package/templates/next/files/next.config.mjs +36 -2
  139. package/templates/next/files/surfaces.js +3 -2
  140. package/templates/next/template.json +9 -3
  141. package/templates/next/files/schema.sql +0 -22
@@ -79,7 +79,7 @@ for a file) are exported too, for your own file rows.
79
79
  ## Example
80
80
 
81
81
  ```jsx
82
- // components/DealFiles.jsx — client
82
+ // components/DealFiles.jsx — client, in a server app
83
83
  "use client";
84
84
  import { FileUpload } from "./blocks/file-upload";
85
85
 
@@ -111,6 +111,12 @@ export default function DealFiles({ deal, onSaveFiles }) {
111
111
  }
112
112
  ```
113
113
 
114
+ A server app's only. `put` needs a route of the app's own to take the
115
+ file (`/api/files` here), and a browser app has none: the data door
116
+ takes no files until uploads have a home. In a browser app, leave
117
+ `onUpload` out and use the block as a picker: read the file in the
118
+ browser, as csv-import does.
119
+
114
120
  One spreadsheet to import, and a profile picture:
115
121
 
116
122
  ```jsx
@@ -364,19 +364,28 @@ one.
364
364
  ## Example
365
365
 
366
366
  ```jsx
367
- // components/LaunchBoard.jsx — client
367
+ // surfaces/launch.jsx — client: one project's board
368
368
  "use client";
369
369
  import { useState } from "react";
370
+ import { useSearchParams } from "next/navigation";
371
+ import { useRow } from "../lib/data";
370
372
  import { updateRow } from "../lib/actions";
371
- import { FlowCanvas } from "./blocks/flow-canvas";
373
+ import { FlowCanvas } from "../components/blocks/flow-canvas";
372
374
 
373
- export default function LaunchBoard({ project }) {
374
- const [board, setBoard] = useState(project.board); // jsonb, may be null
375
+ export default function LaunchSurface() {
376
+ const id = useSearchParams().get("row");
377
+ const { row: project, loading } = useRow({ source: "projects", id });
378
+ if (loading || !project) return null;
379
+ return <LaunchBoard key={project.id} project={project} />;
380
+ }
381
+
382
+ function LaunchBoard({ project }) {
383
+ const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project
375
384
  return (
376
385
  <FlowCanvas
377
386
  value={board ?? undefined}
378
387
  onChange={setBoard}
379
- onSave={(b) => updateRow({ source: "public.projects", id: project.id, patch: { board: b } })}
388
+ onSave={(b) => updateRow({ source: "projects", id: project.id, patch: { board: b } })}
380
389
  label={`${project.name} board`}
381
390
  height="calc(100dvh - 8rem)"
382
391
  />
@@ -384,6 +393,8 @@ export default function LaunchBoard({ project }) {
384
393
  }
385
394
  ```
386
395
 
396
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
397
+
387
398
  A card drawn with kit parts, and an agent reading the board:
388
399
 
389
400
  ```jsx
@@ -3,7 +3,7 @@
3
3
  // FlowCanvas — a Miro-like board on React Flow, in the workspace theme,
4
4
  // behind one set of props (README.md beside this file).
5
5
  //
6
- // Client only, one static bundle: the modules load with the page (the
6
+ // Client only, one bundle: the modules load with the page (the
7
7
  // app's check refuses import()), and the board itself mounts after the
8
8
  // first effect (the mount guard). Until then — on the server and in the
9
9
  // first client paint — a box of the same size holds the board as text, so
@@ -87,7 +87,7 @@ button, named to a screen reader ("X", "LinkedIn"), opening a new tab.
87
87
  "use client";
88
88
  import { Icon } from "./ui";
89
89
  import { Footer } from "./blocks/footer";
90
- import { subscribe } from "../lib/actions"; // your server action
90
+ import { insertRow } from "../lib/actions";
91
91
 
92
92
  export default function SiteFooter() {
93
93
  return (
@@ -102,7 +102,7 @@ export default function SiteFooter() {
102
102
  newsletter={{
103
103
  body: "One email a month.",
104
104
  consent: "I agree to the privacy policy.",
105
- onSubscribe: async (email) => subscribe(email),
105
+ onSubscribe: (email) => insertRow({ source: "subscribers", values: { email } }), // { ok, id } or { ok: false, error }
106
106
  }}
107
107
  legal="© 2026 Fieldline Ltd. All rights reserved."
108
108
  legalLinks={[{ label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }]}
@@ -111,6 +111,8 @@ export default function SiteFooter() {
111
111
  }
112
112
  ```
113
113
 
114
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
115
+
114
116
  ## What it will not do
115
117
 
116
118
  - Change the language or currency by itself: `locales[].onChange` is
@@ -145,15 +145,19 @@ drag saves, as `[{ id, patch }]`), `afterBlockersPatch(task, tasks)`,
145
145
  ## Example
146
146
 
147
147
  ```jsx
148
- // surfaces/plan.jsx — server: load, hand over
149
- import { loadRows } from "../lib/data";
148
+ // surfaces/plan.jsx — client: read, hand over
149
+ "use client";
150
+ import { useRows } from "../lib/data";
150
151
  import Plan from "../components/Plan";
151
152
 
152
- export default async function PlanSurface() {
153
- const { rows, error } = await loadRows({ source: "public.tasks", orderBy: "start asc" });
154
- const people = await loadRows({ source: "public.people" });
155
- const projects = await loadRows({ source: "public.projects" });
156
- return <Plan tasks={rows} people={people.rows} projects={projects.rows} error={error} />;
153
+ export default function PlanSurface() {
154
+ const tasks = useRows({ source: "tasks", orderBy: "start asc" });
155
+ const people = useRows({ source: "people" });
156
+ const projects = useRows({ source: "projects" });
157
+ return (
158
+ <Plan tasks={tasks.rows} people={people.rows} projects={projects.rows}
159
+ loading={tasks.loading || people.loading || projects.loading} error={tasks.error} />
160
+ );
157
161
  }
158
162
  ```
159
163
 
@@ -171,19 +175,19 @@ const COLUMNS = ["id", "title", "status", "assignees", "start", "due", "progress
171
175
  async function save(change) {
172
176
  for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
173
177
  const r =
174
- w.op === "insert" ? await insertRow({ source: "public.tasks", values: w.values, detail: w.values.title })
175
- : w.op === "update" ? await updateRow({ source: "public.tasks", id: w.id, patch: w.patch })
176
- : await deleteRow({ source: "public.tasks", id: w.id });
178
+ w.op === "insert" ? await insertRow({ source: "tasks", values: w.values, detail: w.values.title })
179
+ : w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
180
+ : await deleteRow({ source: "tasks", id: w.id });
177
181
  if (r?.ok === false) return r;
178
182
  }
179
183
  return { ok: true };
180
184
  }
181
185
 
182
- export default function Plan({ tasks, people, projects, error }) {
186
+ export default function Plan({ tasks, people, projects, loading, error }) {
183
187
  const [open, setOpen] = useState(null);
184
188
  return (
185
189
  <>
186
- <Gantt tasks={tasks} people={people} projects={projects} onChange={save} onOpen={setOpen} error={error}
190
+ <Gantt tasks={tasks} people={people} projects={projects} onChange={save} onOpen={setOpen} loading={loading} error={error}
187
191
  starters={[{ label: "Product launch", tasks: [{ title: "Scope", days: 2 }, { title: "Build", days: 10 }, { title: "Launch", milestone: true }] }]} />
188
192
  <Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>{/* the task's fields */}</Sheet>
189
193
  </>
@@ -191,6 +195,8 @@ export default function Plan({ tasks, people, projects, error }) {
191
195
  }
192
196
  ```
193
197
 
198
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
199
+
194
200
  `keepNewIds: false` lets the table make a new task's id. A link made
195
201
  between two tasks created in the same change (a starter) refers to their
196
202
  client ids: keep them (`keepNewIds: true`, a text id column) when you
@@ -166,7 +166,7 @@ export default function PhotoStudio({ photos, reload }) {
166
166
  options={[{ value: "square", label: "Square" }, { value: "portrait", label: "Portrait" }]} />
167
167
  </Field>
168
168
  }
169
- onGenerate={(prompt) => job.start(`Make one ${size} product photo: ${prompt}. Save it to public.photos.`)}
169
+ onGenerate={(prompt) => job.start(`Make one ${size} product photo: ${prompt}. Save it to the photos table.`)}
170
170
  title="Making the photo"
171
171
  doneTitle="Photo saved"
172
172
  results={photos.map((p) => <Image key={p.id} src={p.url} alt={p.prompt} ratio="square" rounded="lg" />)}
@@ -98,24 +98,36 @@ tree on its own (`value`, `onEdit(next, { undo?, message? })`, `open`,
98
98
  ## Example
99
99
 
100
100
  ```jsx
101
+ // surfaces/webhook.jsx — client: one integration's settings
101
102
  "use client";
102
103
  import { useState } from "react";
104
+ import { useSearchParams } from "next/navigation";
105
+ import { useRow } from "../lib/data";
106
+ import { updateRow } from "../lib/actions";
103
107
  import { JsonEditor } from "../components/blocks/json-editor";
104
- import { saveIntegrationSettings } from "../lib/actions"; // your server action → { ok, error? }
105
108
 
106
- export function WebhookSettings({ integration }) {
107
- const [settings, setSettings] = useState(integration.settings);
109
+ export default function WebhookSurface() {
110
+ const id = useSearchParams().get("row");
111
+ const { row: integration, loading } = useRow({ source: "integrations", id });
112
+ if (loading || !integration) return null;
113
+ return <WebhookSettings key={integration.id} integration={integration} />;
114
+ }
115
+
116
+ function WebhookSettings({ integration }) {
117
+ const [settings, setSettings] = useState(integration.settings); // jsonb; read once per integration
108
118
  return (
109
119
  <JsonEditor
110
120
  title="Webhook settings"
111
121
  value={settings}
112
122
  onChange={setSettings}
113
- onSave={(v) => saveIntegrationSettings(integration.id, v)}
123
+ onSave={(v) => updateRow({ source: "integrations", id: integration.id, patch: { settings: v } })}
114
124
  />
115
125
  );
116
126
  }
117
127
  ```
118
128
 
129
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
130
+
119
131
  Inspect a payload, read-only, as text first:
120
132
 
121
133
  ```jsx
@@ -170,10 +170,23 @@ map.current?.getMap(); // the maplibre Map, for what the props do not cover
170
170
 
171
171
  ## Example
172
172
 
173
+ ```jsx
174
+ // surfaces/customers.jsx — client: read, hand over
175
+ "use client";
176
+ import { useRows } from "../lib/data";
177
+ import Customers from "../components/Customers";
178
+
179
+ export default function CustomersSurface() {
180
+ const { rows, error, loading } = useRows({ source: "customers", orderBy: "name asc" });
181
+ if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table
182
+ return <Customers customers={rows} error={error} />;
183
+ }
184
+ ```
185
+
173
186
  ```jsx
174
187
  // components/Customers.jsx — client: the map, the list, the popup
175
188
  "use client";
176
- import { Badge, Button } from "./ui";
189
+ import { Badge, Button, Notice } from "./ui";
177
190
  import { FixtureNotice, useFixture } from "./parts";
178
191
  import { MapView } from "./blocks/map";
179
192
 
@@ -182,10 +195,11 @@ const SAMPLE = [
182
195
  { name: "Acme Bakery", city: "Porto", lat: 41.1405, lng: -8.6132, status: "at risk" },
183
196
  ];
184
197
 
185
- export default function Customers({ customers }) {
198
+ export default function Customers({ customers, error }) {
186
199
  const fixture = useFixture(customers, SAMPLE);
187
200
  return (
188
201
  <div className="grid gap-4">
202
+ {error ? <Notice tone="danger">{error}</Notice> : null}
189
203
  {fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}
190
204
  <MapView
191
205
  items={fixture.rows}
@@ -198,7 +212,7 @@ export default function Customers({ customers }) {
198
212
  <div className="grid gap-2 pr-8">
199
213
  <p className="font-medium">{c.name}</p>
200
214
  <p className="text-muted-foreground">{c.city}</p>
201
- <Button size="sm" variant="primary" href={`/customers/${c.id}`}>Open</Button>
215
+ <Button size="sm" variant="primary" href={`/s/customers?row=${c.id}`}>Open</Button>
202
216
  </div>
203
217
  )}
204
218
  />
@@ -207,6 +221,8 @@ export default function Customers({ customers }) {
207
221
  }
208
222
  ```
209
223
 
224
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
225
+
210
226
  ## What it will not do
211
227
 
212
228
  - Geocode. It maps coordinates; turning an address into them is a
@@ -5,7 +5,7 @@
5
5
  // on them (markers, clusters, the popup) and every control is ours, in the
6
6
  // kit and the workspace's tokens.
7
7
  //
8
- // Client only, one static bundle: maplibre loads with the page (see
8
+ // Client only, one bundle: maplibre loads with the page (see
9
9
  // maplibre.js), and the map itself mounts after the first effect (the
10
10
  // mount guard). Until then — on the server and in the first client paint —
11
11
  // the frame, the list and a quiet placeholder are already there, so the
@@ -11,7 +11,7 @@
11
11
  //
12
12
  // mapLib react-map-gl would otherwise load maplibre-gl with an
13
13
  // import() of its own. Handing it the module we import here
14
- // keeps the app one static bundle.
14
+ // keeps the app one bundle.
15
15
  // WORKER_URL maplibre-gl 6 draws tiles in a Web Worker that it loads
16
16
  // from a file beside its own module (import.meta.url). Inside
17
17
  // an app bundle that URL is a build-time file:// path, so the
@@ -162,20 +162,29 @@ one as it was.
162
162
  ## Example
163
163
 
164
164
  ```jsx
165
- // components/PlanMap.jsx — client
165
+ // surfaces/plan-map.jsx — client: one plan's map
166
166
  "use client";
167
167
  import { useState } from "react";
168
+ import { useSearchParams } from "next/navigation";
169
+ import { useRow } from "../lib/data";
168
170
  import { updateRow } from "../lib/actions";
169
- import { MindMap, mindMapToText } from "./blocks/mind-map";
171
+ import { MindMap, mindMapToText } from "../components/blocks/mind-map";
170
172
 
171
- export default function PlanMap({ plan }) {
172
- const [map, setMap] = useState(plan.map); // jsonb, may be null
173
+ export default function PlanMapSurface() {
174
+ const id = useSearchParams().get("row");
175
+ const { row: plan, loading } = useRow({ source: "plans", id });
176
+ if (loading || !plan) return null;
177
+ return <PlanMap key={plan.id} plan={plan} />;
178
+ }
179
+
180
+ function PlanMap({ plan }) {
181
+ const [map, setMap] = useState(plan.map); // jsonb, may be null; read once per plan
173
182
  return (
174
183
  <MindMap
175
184
  value={map ?? undefined}
176
185
  defaultValue={{ text: plan.name }}
177
186
  onChange={setMap}
178
- onSave={(m) => updateRow({ source: "public.plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
187
+ onSave={(m) => updateRow({ source: "plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
179
188
  label={`${plan.name} map`}
180
189
  className="h-[calc(100dvh-8rem)]"
181
190
  />
@@ -183,6 +192,8 @@ export default function PlanMap({ plan }) {
183
192
  }
184
193
  ```
185
194
 
195
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
196
+
186
197
  An agent's outline, straight onto a map:
187
198
 
188
199
  ```jsx
@@ -106,21 +106,22 @@ One row per person, as a table holds them:
106
106
  ## Example
107
107
 
108
108
  ```jsx
109
- // components/TeamChart.jsx — client
109
+ // surfaces/team.jsx — client: the chart, read and rearranged
110
110
  "use client";
111
- import { useState } from "react";
111
+ import { useRows, whenLanded } from "../lib/data";
112
112
  import { updateRow } from "../lib/actions";
113
- import { OrgChart } from "./blocks/org-chart";
113
+ import { OrgChart } from "../components/blocks/org-chart";
114
114
 
115
- export default function TeamChart({ employees }) {
116
- const [people, setPeople] = useState(employees); // rows of public.employees
115
+ export default function TeamChart() {
116
+ const { rows: people, loading } = useRows({ source: "employees" });
117
+ if (loading) return null;
117
118
  return (
118
119
  <OrgChart
119
120
  people={people}
120
121
  managerKey="manager_id"
121
122
  onChangeManager={async (person, managerId) => {
122
- const res = await updateRow({ source: "public.employees", id: person.id, patch: { manager_id: managerId } });
123
- if (res.ok) setPeople((list) => list.map((p) => (p.id === person.id ? { ...p, manager_id: managerId } : p)));
123
+ const res = await updateRow({ source: "employees", id: person.id, patch: { manager_id: managerId } });
124
+ if (res.ok) await whenLanded(); // the people are read again: the chart draws the new manager
124
125
  return res;
125
126
  }}
126
127
  className="h-[calc(100dvh-8rem)]"
@@ -129,6 +130,8 @@ export default function TeamChart({ employees }) {
129
130
  }
130
131
  ```
131
132
 
133
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
134
+
132
135
  ## What it will not do
133
136
 
134
137
  - Show a person under two managers (dotted lines, matrix orgs): one
@@ -83,7 +83,7 @@ variables the app's Tailwind utilities read. The built-in blocks' CSS
83
83
  workspace theme. Tailwind emits a theme variable only when some source
84
84
  names it, so this block names every token a page may use
85
85
  (`KEEP_TOKENS`). Installing the block is enough.
86
- - **Outside the app** (a download, a static host),
86
+ - **Outside the app** (a download, a plain file host),
87
87
  `standaloneDocument(page, { root })` returns a full HTML document with
88
88
  the tokens resolved and inlined. `pageDocument(page, { tokens })` is the
89
89
  same function for the server, if you pass it the `:root{…}` string
@@ -208,31 +208,38 @@ the helpers the panel uses.
208
208
  ## Example
209
209
 
210
210
  ```jsx
211
+ // surfaces/landing.jsx — client: edit one page
211
212
  "use client";
212
- import { useState } from "react";
213
+ import { useSearchParams } from "next/navigation";
214
+ import { useRow } from "../lib/data";
215
+ import { updateRow } from "../lib/actions";
213
216
  import { PageBuilder, PageView } from "../components/blocks/page-builder";
214
- import { saveRow } from "../lib/db";
215
217
 
216
- export function LandingEditor({ row }) {
218
+ export default function LandingEditor() {
219
+ const id = useSearchParams().get("row");
220
+ const { row, loading } = useRow({ source: "pages", id });
221
+ if (loading || !row) return null;
217
222
  return (
218
223
  <PageBuilder
219
224
  key={row.id}
220
225
  title={row.title}
221
226
  page={row.project ? { project: row.project } : { html: row.html, css: row.css }}
222
227
  onSave={async ({ html, css, project }) => {
223
- const res = await saveRow("pages", row.id, { html, css, project });
228
+ const res = await updateRow({ source: "pages", id: row.id, patch: { html, css, project } });
224
229
  return res.ok ? { ok: true } : { ok: false, error: "The page was not saved. Check your connection and try again." };
225
230
  }}
226
231
  />
227
232
  );
228
233
  }
229
234
 
230
- // the public page, on the server:
235
+ // The finished page, for a surface of its own that reads the row with the same useRow:
231
236
  export function LandingPage({ row }) {
232
237
  return <PageView page={row} label={row.title} />;
233
238
  }
234
239
  ```
235
240
 
241
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
242
+
236
243
  ## What it will not do
237
244
 
238
245
  - No live multiplayer. Two people editing the same page is last save wins.
@@ -12,7 +12,7 @@
12
12
  // GrapesJS runs with <Canvas> from @grapesjs/react, so it creates none of
13
13
  // its own panels; theme.js repaints the few things it still draws.
14
14
  //
15
- // Client only, one static bundle: the modules load with the page (they are
15
+ // Client only, one bundle: the modules load with the page (they are
16
16
  // safe on the server — see grapes.js); the editor is created in an effect
17
17
  // after the mount guard. Until then — on the server and in the first client
18
18
  // paint — the same frame shows with a skeleton.
@@ -27,7 +27,7 @@ export function PageView({ page, label, className }) {
27
27
 
28
28
  /** Browser only: the page as a standalone HTML document with the theme
29
29
  * resolved where `root` sits (colours, faces, radius) — for a download or
30
- * a static host that does not load the app's stylesheet. */
30
+ * a plain file host that does not load the app's stylesheet. */
31
31
  export function standaloneDocument(page, { root, title = "Page", lang = "en" } = {}) {
32
32
  return pageDocument(page, { title, lang, tokens: tokenCss(root), fonts: fontLinks(), fontFaces: fontFaceCss() });
33
33
  }
@@ -91,7 +91,7 @@ export function templatePage(template) {
91
91
  const escapeText = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
92
92
 
93
93
  /** A standalone HTML document for the page — for a download, an email
94
- * preview, a static host. `tokens` is the `:root{…}` rule tokenCss()
94
+ * preview, a plain file host. `tokens` is the `:root{…}` rule tokenCss()
95
95
  * returns in the browser (the theme, resolved); without it the page reads
96
96
  * the variables from whatever stylesheet the host adds. */
97
97
  export function pageDocument(page, { title = "Page", lang = "en", tokens = "", fonts = [], fontFaces = "" } = {}) {
@@ -128,40 +128,42 @@ cancels.
128
128
  ## Example
129
129
 
130
130
  ```jsx
131
+ // surfaces/contract.jsx — client: one document and its highlights
131
132
  "use client";
132
- import { useState } from "react";
133
+ import { useSearchParams } from "next/navigation";
134
+ import { useRow, useRows, whenLanded } from "../lib/data";
135
+ import { deleteRow, insertRow, updateRow } from "../lib/actions";
133
136
  import { PdfViewer } from "../components/blocks/pdf-viewer";
134
- import { insertRow, updateRow, deleteRow } from "../lib/actions";
135
137
 
136
- const source = "public.highlights";
138
+ const source = "highlights";
137
139
 
138
- export function ContractReview({ doc, rows }) {
139
- const [highlights, setHighlights] = useState(rows);
140
+ // A write answers once the highlights are read again, so the viewer never
141
+ // shows the list from before it.
142
+ async function landed(res) {
143
+ if (res.ok) await whenLanded();
144
+ return res;
145
+ }
146
+
147
+ export default function ContractReview() {
148
+ const id = useSearchParams().get("row");
149
+ const { row: doc, loading } = useRow({ source: "documents", id });
150
+ const { rows: highlights } = useRows({ source, filter: { document_id: id } });
151
+ if (loading || !doc) return null;
140
152
  return (
141
153
  <PdfViewer
142
154
  file={doc.url}
143
155
  title={doc.title}
144
156
  highlights={highlights}
145
- onHighlight={async (h) => {
146
- const r = await insertRow({ source, values: { document_id: doc.id, ...h } });
147
- if (r.ok) setHighlights((list) => [...list, { ...h, id: r.id }]);
148
- return r;
149
- }}
150
- onHighlightChange={async (id, patch) => {
151
- const r = await updateRow({ source, id, patch });
152
- if (r.ok) setHighlights((list) => list.map((x) => (x.id === id ? { ...x, ...patch } : x)));
153
- return r;
154
- }}
155
- onHighlightDelete={async (id) => {
156
- const r = await deleteRow({ source, id });
157
- if (r.ok) setHighlights((list) => list.filter((x) => x.id !== id));
158
- return r;
159
- }}
157
+ onHighlight={(h) => insertRow({ source, values: { document_id: doc.id, ...h } }).then(landed)}
158
+ onHighlightChange={(hid, patch) => updateRow({ source, id: hid, patch }).then(landed)}
159
+ onHighlightDelete={(hid) => deleteRow({ source, id: hid }).then(landed)}
160
160
  />
161
161
  );
162
162
  }
163
163
  ```
164
164
 
165
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
166
+
165
167
  Preview a PDF you make (the `pdf-document` block):
166
168
 
167
169
  ```jsx
@@ -6,7 +6,7 @@
6
6
  // callbacks. pdf.js (via react-pdf, see ./pdfjs.js) draws the pages; this
7
7
  // file is the viewer around them, in the kit and the workspace's tokens.
8
8
  //
9
- // Client only, one static bundle. pdf.js needs the DOM and a worker, and
9
+ // Client only, one bundle. pdf.js needs the DOM and a worker, and
10
10
  // the app's check refuses import() (so no next/dynamic): the modules load
11
11
  // with the page (the server half through ssr-shim.js), and the viewer is
12
12
  // created only after the first effect — the mount guard. Until then, on
@@ -19,7 +19,7 @@
19
19
  // expression with that asset's URL (the app's base path included). So the
20
20
  // worker ships with the app, is always the same version as the pdf.js that
21
21
  // calls it, and is served from the app's own origin. No import(), so the
22
- // app's one-static-bundle check is happy.
22
+ // app's one-bundle check (no import()) is happy.
23
23
  //
24
24
  // It is set here, after react-pdf has loaded, because react-pdf sets its
25
25
  // own default when it loads and the later assignment must win. Only in the
@@ -7,7 +7,7 @@
7
7
  // document.createElement("div") (a CSS feature probe), and react-pdf reads
8
8
  // window.location. On the server those throw "window is not defined" and
9
9
  // the page fails to render — though the viewer itself only ever renders in
10
- // the browser (the mount guard). The app is one static bundle (no
10
+ // the browser (the mount guard). The app is one bundle (no
11
11
  // import(), no next/dynamic), so the module cannot be kept off the server;
12
12
  // it is let load against these stand-ins instead — the same trick as
13
13
  // sketch-canvas's ssr-shim.js.
@@ -130,13 +130,16 @@ compareAt, currency })`, `formatPrice(amount, currency, locale)`.
130
130
  ## Example
131
131
 
132
132
  ```jsx
133
- // surfaces/product.jsx — server: load, hand over
134
- import { loadRow } from "../lib/data";
133
+ // surfaces/product.jsx — client: read, hand over
134
+ "use client";
135
+ import { useSearchParams } from "next/navigation";
136
+ import { useRow } from "../lib/data";
135
137
  import ProductPage from "../components/ProductPage";
136
138
 
137
- export default async function ProductSurface({ params }) {
138
- const { row, error } = await loadRow({ source: "public.products", id: params.id });
139
- return <ProductPage product={row} error={error} />;
139
+ export default function ProductSurface() {
140
+ const id = useSearchParams().get("row"); // /s/product?row=<id>
141
+ const { row, error, loading } = useRow({ source: "products", id });
142
+ return <ProductPage product={row} loading={loading} error={error} />;
140
143
  }
141
144
  ```
142
145
 
@@ -146,10 +149,11 @@ export default async function ProductSurface({ params }) {
146
149
  import { insertRow } from "../lib/actions";
147
150
  import { ProductDetail } from "./blocks/product-detail";
148
151
 
149
- export default function ProductPage({ product, error }) {
152
+ export default function ProductPage({ product, loading, error }) {
150
153
  return (
151
154
  <ProductDetail
152
155
  product={product}
156
+ loading={loading}
153
157
  error={error}
154
158
  breadcrumb={[{ label: "Shop", href: "/shop" }, { label: product?.name ?? "Product" }]}
155
159
  perks={[
@@ -158,7 +162,7 @@ export default function ProductPage({ product, error }) {
158
162
  ]}
159
163
  onAddToCart={(line) =>
160
164
  insertRow({
161
- source: "public.cart_items",
165
+ source: "cart_items",
162
166
  values: { product_id: line.id, line_key: line.key, quantity: line.quantity, selected: line.selected },
163
167
  detail: `${line.quantity} × ${line.name}`,
164
168
  })
@@ -168,6 +172,8 @@ export default function ProductPage({ product, error }) {
168
172
  }
169
173
  ```
170
174
 
175
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
176
+
171
177
  A digital product with plans, no quantity, and a straight-to-checkout
172
178
  button:
173
179
 
@@ -142,7 +142,7 @@ const KIT = {
142
142
  key: "licence",
143
143
  label: "Choose your licence",
144
144
  values: [
145
- { value: "html", label: "HTML", price: 39, per: "one-time", description: "Static pages with the styles compiled." },
145
+ { value: "html", label: "HTML", price: 39, per: "one-time", description: "Plain HTML pages with the styles compiled." },
146
146
  { value: "react", label: "React", price: 49, per: "one-time", badge: "Popular", description: "Components with props and typed data." },
147
147
  { value: "team", label: "Team", price: 149, per: "one-time", description: "React, for up to ten people." },
148
148
  ],