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
@@ -144,20 +144,30 @@ on a partial one):
144
144
  ## Example
145
145
 
146
146
  ```jsx
147
- // components/DealNotes.jsx — client
147
+ // surfaces/deal.jsx — client: one deal's notes
148
148
  "use client";
149
149
  import { useState } from "react";
150
+ import { useSearchParams } from "next/navigation";
151
+ import { useRow, useRows } from "../lib/data";
150
152
  import { updateRow } from "../lib/actions";
151
- import { DocEditor } from "./blocks/doc-editor";
153
+ import { DocEditor } from "../components/blocks/doc-editor";
154
+
155
+ export default function DealSurface() {
156
+ const id = useSearchParams().get("row");
157
+ const { row: deal, loading } = useRow({ source: "deals", id });
158
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
159
+ if (loading || !deal) return null;
160
+ return <DealNotes key={deal.id} deal={deal} team={team} />;
161
+ }
152
162
 
153
- export default function DealNotes({ deal, team }) {
154
- const [doc, setDoc] = useState(deal.notes); // jsonb, may be null
163
+ function DealNotes({ deal, team }) {
164
+ const [doc, setDoc] = useState(deal.notes); // jsonb, may be null; read once per deal, so a read never moves the cursor
155
165
  return (
156
166
  <DocEditor
157
167
  value={doc}
158
168
  onChange={(d) => {
159
169
  setDoc(d);
160
- updateRow({ source: "public.deals", id: deal.id, patch: { notes: d } }); // debounce this in a real app
170
+ updateRow({ source: "deals", id: deal.id, patch: { notes: d } }); // debounce this in a real app
161
171
  }}
162
172
  mentions={async (q) => team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))}
163
173
  placeholder="Notes on this deal…"
@@ -167,13 +177,15 @@ export default function DealNotes({ deal, team }) {
167
177
  }
168
178
  ```
169
179
 
180
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
181
+
170
182
  A comment box that posts on Cmd/Ctrl+Enter and clears itself:
171
183
 
172
184
  ```jsx
173
185
  const box = useDocEditor();
174
186
  const post = async (doc) => {
175
187
  if (isDocEmpty(doc)) return;
176
- const res = await insertRow({ source: "public.comments", values: { body: doc, body_text: docToText(doc) } });
188
+ const res = await insertRow({ source: "comments", values: { body: doc, body_text: docToText(doc) } });
177
189
  if (res.ok) box.current?.clear();
178
190
  };
179
191
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  // DocEditor — BlockNote, in the workspace theme, behind one set of props.
4
4
  //
5
- // Client only, one static bundle. BlockNote needs the DOM, and the app's
5
+ // Client only, one bundle. BlockNote needs the DOM, and the app's
6
6
  // check refuses import() (so no next/dynamic): the modules load with the
7
7
  // page, and the editor itself is created only after the first effect
8
8
  // (the mount guard). Until then — on the server and in the first client
@@ -75,22 +75,27 @@ out without waiting for the answer.
75
75
  ## Example
76
76
 
77
77
  ```jsx
78
- // components/WikiPage.jsx — client
78
+ // surfaces/wiki.jsx — client: one page, read and saved
79
79
  "use client";
80
+ import { useSearchParams } from "next/navigation";
81
+ import { useRow, useRows } from "../lib/data";
80
82
  import { updateRow } from "../lib/actions";
81
- import { DocPage } from "./blocks/doc-page";
83
+ import { DocPage } from "../components/blocks/doc-page";
82
84
 
83
- export default function WikiPage({ page, team, canEdit }) {
85
+ export default function WikiPage() {
86
+ const id = useSearchParams().get("row");
87
+ const { row: page, loading } = useRow({ source: "wiki_pages", id });
88
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
89
+ if (loading || !page) return null;
84
90
  return (
85
91
  <DocPage
86
92
  id={page.id}
87
93
  title={page.title}
88
94
  icon={page.icon}
89
95
  doc={page.body}
90
- readOnly={!canEdit}
91
- meta={`Edited by ${page.edited_by_name}`}
96
+ meta={`Edited by ${page.edited_by}`} // a before-update trigger sets edited_by (the app's README §5)
92
97
  onSave={({ id, title, icon, doc }) =>
93
- updateRow({ source: "public.wiki_pages", id, patch: { title, icon, body: doc } })
98
+ updateRow({ source: "wiki_pages", id, patch: { title, icon, body: doc } })
94
99
  }
95
100
  mentions={async (q) =>
96
101
  team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }))
@@ -100,6 +105,11 @@ export default function WikiPage({ page, team, canEdit }) {
100
105
  }
101
106
  ```
102
107
 
108
+ The page reads `title`, `icon` and `doc` when `id` changes and owns the
109
+ draft in between, so the read after each save never moves the cursor.
110
+
111
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
112
+
103
113
  For search or a preview, keep a plain-text copy beside the doc:
104
114
  `docToText(doc)` from the doc-editor block, saved in the same `onSave`.
105
115
 
@@ -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
@@ -111,7 +111,10 @@ re-route as cards move; handle ids are not stored.
111
111
  screen). `onExitPresentation()` — Esc and the bar's Exit call it; turn
112
112
  `presentation` off there.
113
113
  - `height` — the canvas's height, default `560` (px; any CSS length works:
114
- `"70vh"`, `"100%"` inside a sized box).
114
+ `"100%"` inside a sized box, such as the kit's `AppLayout
115
+ contentMode="fill"` for a board that is the whole surface, the
116
+ Example). Never a `vh` length: `npm run check` refuses a size taken
117
+ from the window.
115
118
  - `bare` — `false` (default: rounded, bordered). `true`: no border or
116
119
  radius, for a full-page board.
117
120
  - `label` — the board's accessible name, default `"Board"`; also the list
@@ -364,26 +367,41 @@ one.
364
367
  ## Example
365
368
 
366
369
  ```jsx
367
- // components/LaunchBoard.jsx — client
370
+ // surfaces/launch.jsx — client: one project's board
368
371
  "use client";
369
372
  import { useState } from "react";
373
+ import { useSearchParams } from "next/navigation";
374
+ import { useRow } from "../lib/data";
370
375
  import { updateRow } from "../lib/actions";
371
- import { FlowCanvas } from "./blocks/flow-canvas";
376
+ import { AppLayout } from "../components/ui";
377
+ import { FlowCanvas } from "../components/blocks/flow-canvas";
378
+
379
+ export default function LaunchSurface() {
380
+ const id = useSearchParams().get("row");
381
+ const { row: project, loading } = useRow({ source: "projects", id });
382
+ if (loading || !project) return null;
383
+ return <LaunchBoard key={project.id} project={project} />;
384
+ }
372
385
 
373
- export default function LaunchBoard({ project }) {
374
- const [board, setBoard] = useState(project.board); // jsonb, may be null
386
+ function LaunchBoard({ project }) {
387
+ const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project
375
388
  return (
376
- <FlowCanvas
377
- value={board ?? undefined}
378
- onChange={setBoard}
379
- onSave={(b) => updateRow({ source: "public.projects", id: project.id, patch: { board: b } })}
380
- label={`${project.name} board`}
381
- height="calc(100dvh - 8rem)"
382
- />
389
+ <AppLayout contentMode="fill">
390
+ <FlowCanvas
391
+ value={board ?? undefined}
392
+ onChange={setBoard}
393
+ onSave={(b) => updateRow({ source: "projects", id: project.id, patch: { board: b } })}
394
+ label={`${project.name} board`}
395
+ height="100%"
396
+ bare
397
+ />
398
+ </AppLayout>
383
399
  );
384
400
  }
385
401
  ```
386
402
 
403
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
404
+
387
405
  A card drawn with kit parts, and an agent reading the board:
388
406
 
389
407
  ```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,12 +111,15 @@ 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
117
119
  yours.
118
- - Stick to the bottom of a short page: that is your layout's
119
- `min-h-dvh` flex column.
120
+ - Stick to the bottom of a short page: that is your layout's. The kit's
121
+ `AppLayout` content is a flex column, so a `flex-1` above the footer
122
+ pushes it down on a short page and lets it follow a long one.
120
123
  - Paint itself in the primary colour: the look is the workspace
121
124
  theme's.
122
125
  - Draw store badges: "Get the app" is two buttons with the Apple and
@@ -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
@@ -96,7 +96,9 @@ string, or nothing (a fresh map). The component runs it on `value`.
96
96
  - `placeholder` — what an empty topic shows while it is typed (default
97
97
  "New topic").
98
98
  - `height` — px or any CSS length, default `600`. A height class in
99
- `className` (`h-dvh`, `h-full`, `flex-1`) replaces it.
99
+ `className` (`h-full`, `min-h-0 flex-1`) replaces it: `min-h-0 flex-1`
100
+ fills what the surface has left (the Example). Never `h-dvh` or a `vh`
101
+ height, which `npm run check` refuses.
100
102
  - `label` — the map's accessible name (default "Mind map").
101
103
  - `onSelectionChange(topic | null)` — the selected topic changed (a side
102
104
  panel with its notes).
@@ -162,27 +164,38 @@ one as it was.
162
164
  ## Example
163
165
 
164
166
  ```jsx
165
- // components/PlanMap.jsx — client
167
+ // surfaces/plan-map.jsx — client: one plan's map
166
168
  "use client";
167
169
  import { useState } from "react";
170
+ import { useSearchParams } from "next/navigation";
171
+ import { useRow } from "../lib/data";
168
172
  import { updateRow } from "../lib/actions";
169
- import { MindMap, mindMapToText } from "./blocks/mind-map";
173
+ import { MindMap, mindMapToText } from "../components/blocks/mind-map";
170
174
 
171
- export default function PlanMap({ plan }) {
172
- const [map, setMap] = useState(plan.map); // jsonb, may be null
175
+ export default function PlanMapSurface() {
176
+ const id = useSearchParams().get("row");
177
+ const { row: plan, loading } = useRow({ source: "plans", id });
178
+ if (loading || !plan) return null;
179
+ return <PlanMap key={plan.id} plan={plan} />;
180
+ }
181
+
182
+ function PlanMap({ plan }) {
183
+ const [map, setMap] = useState(plan.map); // jsonb, may be null; read once per plan
173
184
  return (
174
185
  <MindMap
175
186
  value={map ?? undefined}
176
187
  defaultValue={{ text: plan.name }}
177
188
  onChange={setMap}
178
- onSave={(m) => updateRow({ source: "public.plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
189
+ onSave={(m) => updateRow({ source: "plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
179
190
  label={`${plan.name} map`}
180
- className="h-[calc(100dvh-8rem)]"
191
+ className="min-h-0 flex-1"
181
192
  />
182
193
  );
183
194
  }
184
195
  ```
185
196
 
197
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
198
+
186
199
  An agent's outline, straight onto a map:
187
200
 
188
201
  ```jsx
@@ -73,7 +73,9 @@ One row per person, as a table holds them:
73
73
  reports: kit parts (their open deals, a "Message" button).
74
74
  - `onSelectionChange(person | null)` — the selected card changed.
75
75
  - `height` — px or any CSS length, default `640`. A height class in
76
- `className` (`h-dvh`, `h-full`, `flex-1`) replaces it.
76
+ `className` (`h-full`, `min-h-0 flex-1`) replaces it: `min-h-0 flex-1`
77
+ fills what the surface has left (the Example). Never `h-dvh` or a `vh`
78
+ height, which `npm run check` refuses.
77
79
  - `label` — the chart's accessible name (default "Org chart").
78
80
  - `className` — layout around it (margin, width, height). Never colour.
79
81
 
@@ -106,29 +108,32 @@ One row per person, as a table holds them:
106
108
  ## Example
107
109
 
108
110
  ```jsx
109
- // components/TeamChart.jsx — client
111
+ // surfaces/team.jsx — client: the chart, read and rearranged
110
112
  "use client";
111
- import { useState } from "react";
113
+ import { useRows, whenLanded } from "../lib/data";
112
114
  import { updateRow } from "../lib/actions";
113
- import { OrgChart } from "./blocks/org-chart";
115
+ import { OrgChart } from "../components/blocks/org-chart";
114
116
 
115
- export default function TeamChart({ employees }) {
116
- const [people, setPeople] = useState(employees); // rows of public.employees
117
+ export default function TeamChart() {
118
+ const { rows: people, loading } = useRows({ source: "employees" });
119
+ if (loading) return null;
117
120
  return (
118
121
  <OrgChart
119
122
  people={people}
120
123
  managerKey="manager_id"
121
124
  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)));
125
+ const res = await updateRow({ source: "employees", id: person.id, patch: { manager_id: managerId } });
126
+ if (res.ok) await whenLanded(); // the people are read again: the chart draws the new manager
124
127
  return res;
125
128
  }}
126
- className="h-[calc(100dvh-8rem)]"
129
+ className="min-h-0 flex-1"
127
130
  />
128
131
  );
129
132
  }
130
133
  ```
131
134
 
135
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
136
+
132
137
  ## What it will not do
133
138
 
134
139
  - 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
@@ -105,7 +105,7 @@ variables the app's Tailwind utilities read. The built-in blocks' CSS
105
105
  | `actions` | — | Extra top-bar controls before Save, such as landing-builder's Publish. Use kit `Button`s at `size="sm"`. |
106
106
  | `saveLabel` | `"Save"` | The Save button's words. |
107
107
  | `builderRef` | — | A ref from `usePageBuilder()`. It receives the handle. |
108
- | `height` | `720` | The builder's height: a number (px) or any CSS length (`"min(760px, 88dvh)"`). It fills its width. |
108
+ | `height` | `720` | The builder's height: a number (px) or any CSS length (`"100%"` in a sized box, such as the kit's `AppLayout contentMode="fill"`; never a `vh` length, which `npm run check` refuses). It fills its width. |
109
109
  | `className` | — | Added to the frame. |
110
110
 
111
111
  ### The handle
@@ -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.
@@ -78,7 +78,7 @@ export default function PageBuilderDemo() {
78
78
  <Landing />
79
79
  </TabPanel>
80
80
  <TabPanel tab="fresh" value={tab}>
81
- <PageBuilder title="New page" height="min(720px, 88dvh)" />
81
+ <PageBuilder title="New page" height={720} />
82
82
  </TabPanel>
83
83
  <TabPanel tab="own" value={tab}>
84
84
  <PageBuilder
@@ -86,7 +86,7 @@ export default function PageBuilderDemo() {
86
86
  builtIns="basics"
87
87
  blocks={OWN_BLOCKS}
88
88
  templates={OWN_TEMPLATES}
89
- height="min(720px, 88dvh)"
89
+ height={720}
90
90
  onSave={async () => {
91
91
  await new Promise((r) => setTimeout(r, 400));
92
92
  toast.success("Page saved");
@@ -122,7 +122,7 @@ function Landing() {
122
122
  page={{ html: LAUNCH.html }}
123
123
  onSave={onSave}
124
124
  builderRef={builder}
125
- height="min(760px, 88dvh)"
125
+ height={760}
126
126
  onUploadImage={async (file) => {
127
127
  await new Promise((r) => setTimeout(r, 500));
128
128
  // In an app: upload to storage and return its public address.
@@ -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.