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
@@ -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 = "" } = {}) {
@@ -108,7 +108,10 @@ Store a highlight as a row in its own table (`document_id`, `page`,
108
108
  - `notes` — open the Highlights list on a wide viewer at first. Default:
109
109
  open when there are highlights.
110
110
  - `height` — the viewer's height, any CSS length. Default
111
- `"min(80vh, 860px)"`; it never goes under 384px.
111
+ `"min(80vh, 860px)"` (the block's own); it never goes under 384px.
112
+ Yours is a length, or `"100%"` in a sized box such as the kit's
113
+ `AppLayout contentMode="fill"`: a `vh` length in your code is refused
114
+ by `npm run check`.
112
115
  - `loading` — `true` shows the skeleton (while you fetch or make the
113
116
  file) instead of the empty state.
114
117
  - `emptyTitle` · `emptyBody` — the words when `file` is empty. Default
@@ -128,40 +131,42 @@ cancels.
128
131
  ## Example
129
132
 
130
133
  ```jsx
134
+ // surfaces/contract.jsx — client: one document and its highlights
131
135
  "use client";
132
- import { useState } from "react";
136
+ import { useSearchParams } from "next/navigation";
137
+ import { useRow, useRows, whenLanded } from "../lib/data";
138
+ import { deleteRow, insertRow, updateRow } from "../lib/actions";
133
139
  import { PdfViewer } from "../components/blocks/pdf-viewer";
134
- import { insertRow, updateRow, deleteRow } from "../lib/actions";
135
140
 
136
- const source = "public.highlights";
141
+ const source = "highlights";
137
142
 
138
- export function ContractReview({ doc, rows }) {
139
- const [highlights, setHighlights] = useState(rows);
143
+ // A write answers once the highlights are read again, so the viewer never
144
+ // shows the list from before it.
145
+ async function landed(res) {
146
+ if (res.ok) await whenLanded();
147
+ return res;
148
+ }
149
+
150
+ export default function ContractReview() {
151
+ const id = useSearchParams().get("row");
152
+ const { row: doc, loading } = useRow({ source: "documents", id });
153
+ const { rows: highlights } = useRows({ source, filter: { document_id: id } });
154
+ if (loading || !doc) return null;
140
155
  return (
141
156
  <PdfViewer
142
157
  file={doc.url}
143
158
  title={doc.title}
144
159
  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
- }}
160
+ onHighlight={(h) => insertRow({ source, values: { document_id: doc.id, ...h } }).then(landed)}
161
+ onHighlightChange={(hid, patch) => updateRow({ source, id: hid, patch }).then(landed)}
162
+ onHighlightDelete={(hid) => deleteRow({ source, id: hid }).then(landed)}
160
163
  />
161
164
  );
162
165
  }
163
166
  ```
164
167
 
168
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
169
+
165
170
  Preview a PDF you make (the `pdf-document` block):
166
171
 
167
172
  ```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
  ],
@@ -84,37 +84,37 @@ quantities by `key`.
84
84
  ## Example
85
85
 
86
86
  ```jsx
87
- // surfaces/shop.jsx — server: load, hand over
88
- import { loadRows } from "../lib/data";
87
+ // surfaces/shop.jsx — client: read, hand over
88
+ "use client";
89
+ import { useRows } from "../lib/data";
89
90
  import Shop from "../components/Shop";
90
91
 
91
- export default async function ShopSurface() {
92
- const { rows, error } = await loadRows({ source: "public.products", orderBy: "created_at desc" });
93
- return <Shop products={rows} error={error} />;
92
+ export default function ShopSurface() {
93
+ const { rows, error, loading, reload } = useRows({ source: "products", orderBy: "created_at desc" });
94
+ return <Shop products={rows} loading={loading} error={error} onRetry={reload} />;
94
95
  }
95
96
  ```
96
97
 
97
98
  ```jsx
98
99
  // components/Shop.jsx — client: the grid and the cart write
99
100
  "use client";
100
- import { useRouter } from "next/navigation";
101
101
  import { insertRow } from "../lib/actions";
102
102
  import { ProductGrid } from "./blocks/product-grid";
103
103
 
104
- export default function Shop({ products, error }) {
105
- const router = useRouter();
104
+ export default function Shop({ products, loading, error, onRetry }) {
106
105
  return (
107
106
  <ProductGrid
108
107
  products={products}
108
+ loading={loading}
109
109
  error={error}
110
- onRetry={() => router.refresh()}
110
+ onRetry={onRetry}
111
111
  filters={[
112
112
  { key: "category", label: "Category", options: ["Shirts", "Sweaters", "Jackets"] },
113
113
  { key: "size", label: "Size", options: ["S", "M", "L", "XL"] },
114
114
  ]}
115
115
  sort
116
116
  onAddToCart={(line) =>
117
- insertRow({ source: "public.cart_items", values: { product_id: line.id, line_key: line.key, quantity: 1 }, detail: line.name })
117
+ insertRow({ source: "cart_items", values: { product_id: line.id, line_key: line.key, quantity: 1 }, detail: line.name })
118
118
  }
119
119
  label="Shop"
120
120
  />
@@ -122,6 +122,8 @@ export default function Shop({ products, error }) {
122
122
  }
123
123
  ```
124
124
 
125
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
126
+
125
127
  Collection tiles, and a lead picture beside four products:
126
128
 
127
129
  ```jsx
@@ -93,7 +93,7 @@ Validation, first broken rule wins, one message per field:
93
93
  error?, errors? }` where `errors` is `{ fieldName: "message" }` — each
94
94
  shows under its field and the first one is focused. A throw counts as
95
95
  failed. In a Makersclaw app it is one line over `updateRow`:
96
- `onSave={(v) => updateRow({ source: "public.products", id: p.id, patch: v })}`.
96
+ `onSave={(v) => updateRow({ source: "products", id: p.id, patch: v })}`.
97
97
  - `container` — `"card"` (default: a Card with `title` and `description`)
98
98
  · `"page"` (no box: a page heading, then the sections) · `"dialog"` (a
99
99
  centred Sheet; on a phone a bottom sheet). A dialog needs `open` and
@@ -127,10 +127,12 @@ Validation, first broken rule wins, one message per field:
127
127
  ## Example
128
128
 
129
129
  ```jsx
130
- // components/ProductEditor.jsx — client
130
+ // surfaces/product.jsx — client: one product, edited
131
131
  "use client";
132
- import { updateRow, deleteRow } from "../lib/actions";
133
- import { RecordForm } from "./blocks/record-form";
132
+ import { useRouter, useSearchParams } from "next/navigation";
133
+ import { useRow } from "../lib/data";
134
+ import { deleteRow, updateRow } from "../lib/actions";
135
+ import { RecordForm } from "../components/blocks/record-form";
134
136
 
135
137
  const FIELDS = [
136
138
  { name: "name", label: "Product name", required: true },
@@ -140,20 +142,35 @@ const FIELDS = [
140
142
  { name: "details", label: "Details", type: "textarea" },
141
143
  ];
142
144
 
143
- export default function ProductEditor({ product }) {
145
+ export default function ProductEditor() {
146
+ const router = useRouter();
147
+ const id = useSearchParams().get("row");
148
+ const { row: product, loading, error, reload } = useRow({ source: "products", id });
144
149
  return (
145
150
  <RecordForm
146
151
  title="Edit product"
147
152
  fields={FIELDS}
148
- values={product}
149
- onSave={(values) => updateRow({ source: "public.products", id: product.id, patch: values })}
150
- onDelete={() => deleteRow({ source: "public.products", id: product.id })}
153
+ values={product ?? {}}
154
+ loading={loading}
155
+ error={error}
156
+ onRetry={reload}
157
+ onSave={(values) => updateRow({ source: "products", id, patch: values })}
158
+ onDelete={async () => {
159
+ const res = await deleteRow({ source: "products", id });
160
+ if (res.ok) router.push("/s/products");
161
+ return res;
162
+ }}
151
163
  deleteLabel="Remove product"
152
164
  />
153
165
  );
154
166
  }
155
167
  ```
156
168
 
169
+ After a save the row is read again and comes back as `values`: the
170
+ form starts over from what it just saved.
171
+
172
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
173
+
157
174
  In a dialog, from a list:
158
175
 
159
176
  ```jsx
@@ -170,7 +187,7 @@ const [editing, setEditing] = useState(null);
170
187
  { title: "Access", fields: [{ name: "role", label: "Role", type: "radio", options: ["Admin", "Member", "Viewer"] }] },
171
188
  ]}
172
189
  onSave={async (values) => {
173
- const res = await updateRow({ source: "public.members", id: editing.id, patch: values });
190
+ const res = await updateRow({ source: "members", id: editing.id, patch: values });
174
191
  return res.ok ? res : { ok: false, errors: res.error?.includes("email") ? { email: "Already in use." } : undefined, error: res.error };
175
192
  }}
176
193
  />
@@ -73,45 +73,57 @@ One table, a row per note: `record_id`, `author_id`, `body jsonb`,
73
73
  ## Example
74
74
 
75
75
  ```jsx
76
- // components/DealNotes.jsx — client
76
+ // surfaces/deal-notes.jsx — client: one deal's notes
77
77
  "use client";
78
- import { useState } from "react";
78
+ import { useSearchParams } from "next/navigation";
79
+ import { useArtifact } from "../lib/hooks";
80
+ import { useRows, whenLanded } from "../lib/data";
79
81
  import { deleteRow, insertRow, updateRow } from "../lib/actions";
80
- import { RecordNotes } from "./blocks/record-notes";
81
- import { docToText } from "./blocks/doc-editor";
82
+ import { RecordNotes } from "../components/blocks/record-notes";
83
+ import { docToText } from "../components/blocks/doc-editor";
82
84
 
83
- export default function DealNotes({ deal, me, team, initial }) {
84
- const [notes, setNotes] = useState(initial);
85
+ const source = "deal_notes";
86
+
87
+ // A write answers once the notes are read again, so the list never shows
88
+ // the notes from before it.
89
+ async function landed(res) {
90
+ if (res.ok) await whenLanded();
91
+ return res;
92
+ }
93
+
94
+ export default function DealNotes() {
95
+ const dealId = useSearchParams().get("row");
96
+ const { user } = useArtifact(); // the member signed in: { id, email }
97
+ const { rows, loading } = useRows({ source, filter: { deal_id: dealId }, orderBy: "created_at desc" });
98
+ const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
99
+ const me = { id: user?.email, name: user?.email ?? "You" };
100
+ // created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
101
+ const notes = rows.map((n) => ({
102
+ id: n.id,
103
+ author: { id: n.created_by, name: n.created_by },
104
+ body: n.body,
105
+ createdAt: n.created_at,
106
+ updatedAt: n.updated_at,
107
+ }));
85
108
  const findPeople = async (q) =>
86
109
  team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
87
110
 
88
- async function add(doc) {
89
- const res = await insertRow({
90
- source: "public.deal_notes",
91
- values: { deal_id: deal.id, author_id: me.id, body: doc, body_text: docToText(doc) },
92
- });
93
- if (!res.ok) return res;
94
- setNotes((n) => [{ id: res.id, author: me, body: doc, createdAt: new Date() }, ...n]);
95
- return { ok: true };
96
- }
97
-
98
- async function edit(note, doc) {
99
- const updatedAt = new Date();
100
- const res = await updateRow({ source: "public.deal_notes", id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: updatedAt } });
101
- if (res.ok) setNotes((n) => n.map((x) => (x.id === note.id ? { ...x, body: doc, updatedAt } : x)));
102
- return res;
103
- }
104
-
105
- async function remove(note) {
106
- const res = await deleteRow({ source: "public.deal_notes", id: note.id });
107
- if (res.ok) setNotes((n) => n.filter((x) => x.id !== note.id));
108
- return res;
109
- }
110
-
111
- return <RecordNotes notes={notes} me={me} onAdd={add} onEdit={edit} onDelete={remove} mentions={findPeople} />;
111
+ if (loading) return null;
112
+ return (
113
+ <RecordNotes
114
+ notes={notes}
115
+ me={me}
116
+ onAdd={(doc) => insertRow({ source, values: { deal_id: dealId, body: doc, body_text: docToText(doc) } }).then(landed)}
117
+ onEdit={(note, doc) => updateRow({ source, id: note.id, patch: { body: doc, body_text: docToText(doc), updated_at: new Date() } }).then(landed)}
118
+ onDelete={(note) => deleteRow({ source, id: note.id }).then(landed)}
119
+ mentions={findPeople}
120
+ />
121
+ );
112
122
  }
113
123
  ```
114
124
 
125
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
126
+
115
127
  Beside the form, on a record page:
116
128
 
117
129
  ```jsx
@@ -70,15 +70,29 @@ Both components take these.
70
70
  ## Example
71
71
 
72
72
  ```jsx
73
+ // components/GlobalSearch.jsx — client: search as the member types
73
74
  "use client";
74
- import { useRouter } from "next/navigation";
75
75
  import { useState } from "react";
76
- import { Button } from "../components/ui";
77
- import { SearchDialog } from "../components/blocks/search-dialog";
76
+ import { useRouter } from "next/navigation";
77
+ import { loadRows, useRows } from "../lib/data";
78
+ import { Button } from "./ui";
79
+ import { SearchDialog } from "./blocks/search-dialog";
80
+
81
+ const asResult = (d) => ({ id: d.id, kind: "deal", title: d.name, snippet: d.stage });
78
82
 
79
- export function GlobalSearch({ recent, search }) {
83
+ export default function GlobalSearch() {
80
84
  const router = useRouter();
81
85
  const [open, setOpen] = useState(false);
86
+ const { rows: recent } = useRows({ source: "deals", orderBy: "updated_at desc", limit: 5 });
87
+
88
+ // The app's own table: names that start with what was typed, case ignored.
89
+ // The dialog aborts the older query through `signal`.
90
+ async function search(q, { signal }) {
91
+ const { rows, error } = await loadRows({ source: "deals", filter: { name: { prefix: q } }, orderBy: "name asc", limit: 20, signal });
92
+ if (error) throw new Error(error); // "Search failed"; an aborted read's answer is ignored
93
+ return rows.map(asResult);
94
+ }
95
+
82
96
  return (
83
97
  <>
84
98
  <Button icon="search" onClick={() => setOpen(true)}>Search</Button>
@@ -86,15 +100,32 @@ export function GlobalSearch({ recent, search }) {
86
100
  open={open}
87
101
  onOpenChange={setOpen}
88
102
  hotkey="k"
89
- sections={[{ heading: "Recent", results: recent }]}
90
- onSearch={(q) => search(q)} // a server action returning results
91
- onSelect={(r) => router.push(`/${r.kind}s/${r.id}`)}
103
+ sections={[{ heading: "Recent", results: recent.map(asResult) }]}
104
+ onSearch={search}
105
+ onSelect={(r) => router.push(`/s/deals?row=${r.id}`)}
92
106
  />
93
107
  </>
94
108
  );
95
109
  }
96
110
  ```
97
111
 
112
+ The company's records, through the brain (`lib/brain.js`), with
113
+ `meaning: false` while a person types: keywords only, nothing embedded
114
+ or charged. A search by meaning is charged per call; keep it for a
115
+ search page or an Enter.
116
+
117
+ ```jsx
118
+ import { searchBrain } from "../lib/brain";
119
+
120
+ async function search(q, { signal }) {
121
+ const { results, error } = await searchBrain({ q, meaning: false, limit: 20, signal });
122
+ if (error) throw new Error(error);
123
+ return results.map(({ record, snippet }) => ({ id: record.id, kind: record.kind, title: record.label, snippet }));
124
+ }
125
+ ```
126
+
127
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
128
+
98
129
  ## What it will not do
99
130
 
100
131
  - Index anything. It searches the list you give it, or asks your server.
@@ -71,7 +71,7 @@ The matrix (`variant="matrix"`) rows are events:
71
71
  false, error }`; a throw counts as failed. Without it, switches and
72
72
  selects are shown and cannot be changed. In a Makersclaw app it is one
73
73
  line over `saveSetting` (lib/actions.js):
74
- `onChange={(row, value) => saveSetting({ source: "public.settings", key: row.key, value })}`.
74
+ `onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}`.
75
75
  - `onAction(row, actionKey)` — run a button, a menu item or a remove.
76
76
  Return `{ ok }` like `onChange`. Remove the row from `rows` on success
77
77
  when it is gone (a signed-out device).
@@ -95,31 +95,43 @@ The matrix (`variant="matrix"`) rows are events:
95
95
  ## Example
96
96
 
97
97
  ```jsx
98
- // components/Preferences.jsx — client
98
+ // surfaces/preferences.jsx — client: settings, read and saved
99
99
  "use client";
100
+ import { useMemo } from "react";
101
+ import { useRows } from "../lib/data";
100
102
  import { saveSetting } from "../lib/actions";
101
- import { SettingsList } from "./blocks/settings-list";
102
-
103
- export default function Preferences({ settings }) {
104
- const rows = [
105
- { key: "digest", icon: "mail", label: "Weekly digest", description: "Every Monday at 8:00.", control: { type: "switch", value: settings.digest === true } },
106
- {
107
- key: "currency",
108
- icon: "dollar",
109
- label: "Currency",
110
- control: { type: "select", value: settings.currency ?? "usd", options: [{ value: "usd", label: "US dollar" }, { value: "eur", label: "Euro" }] },
111
- },
112
- ];
103
+ import { SettingsList } from "../components/blocks/settings-list";
104
+
105
+ export default function Preferences() {
106
+ const { rows: saved, loading, error, reload } = useRows({ source: "settings" });
107
+ // Built once per read: until `rows` changes, a row shows the value it saved.
108
+ const rows = useMemo(() => {
109
+ const settings = Object.fromEntries(saved.map((s) => [s.key, s.value]));
110
+ return [
111
+ { key: "digest", icon: "mail", label: "Weekly digest", description: "Every Monday at 8:00.", control: { type: "switch", value: settings.digest === true } },
112
+ {
113
+ key: "currency",
114
+ icon: "dollar",
115
+ label: "Currency",
116
+ control: { type: "select", value: settings.currency ?? "usd", options: [{ value: "usd", label: "US dollar" }, { value: "eur", label: "Euro" }] },
117
+ },
118
+ ];
119
+ }, [saved]);
113
120
  return (
114
121
  <SettingsList
115
122
  title="Preferences"
116
123
  rows={rows}
117
- onChange={(row, value) => saveSetting({ source: "public.settings", key: row.key, value })}
124
+ loading={loading}
125
+ error={error}
126
+ onRetry={reload}
127
+ onChange={(row, value) => saveSetting({ source: "settings", key: row.key, value })}
118
128
  />
119
129
  );
120
130
  }
121
131
  ```
122
132
 
133
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
134
+
123
135
  Notifications per channel:
124
136
 
125
137
  ```jsx
@@ -131,7 +143,7 @@ Notifications per channel:
131
143
  { key: "mentions", group: "Conversations", label: "Mentions", values: { email: true, push: true } },
132
144
  { key: "signin", group: "Security", label: "New sign-in", values: { email: true, push: false }, locked: ["email"] },
133
145
  ]}
134
- onChange={(row, on, channel) => saveSetting({ mode: "json", source: "public.members", column: "notify", id: me.id, key: `${row.key}.${channel}`, value: on })}
146
+ onChange={(row, on, channel) => saveSetting({ mode: "json", source: "members", column: "notify", id: me.id, key: `${row.key}.${channel}`, value: on })}
135
147
  />
136
148
  ```
137
149
 
@@ -149,7 +161,7 @@ Signed-in devices, each with sign-out:
149
161
  meta: [{ icon: "globe", text: s.place }, { icon: "clock", text: s.lastSeen }],
150
162
  control: s.current ? null : { type: "remove", label: `Sign out ${s.device}`, confirm: { title: "Sign out this device?", confirmLabel: "Sign out" } },
151
163
  }))}
152
- onAction={(row) => deleteRow({ source: "public.sessions", id: row.key })}
164
+ onAction={(row) => deleteRow({ source: "sessions", id: row.key })}
153
165
  />
154
166
  ```
155
167
 
@@ -75,16 +75,15 @@ With no steps it says "Nothing to set up".
75
75
  ## Example
76
76
 
77
77
  ```jsx
78
- // surfaces/home.jsx — server: work out what is done
79
- import { loadRows } from "../lib/data";
78
+ // surfaces/home.jsx — client: work out what is done
79
+ "use client";
80
+ import { useCount } from "../lib/data";
80
81
  import GettingStarted from "../components/GettingStarted";
81
82
 
82
- export default async function Home() {
83
- const [{ rows: members }, { rows: deals }] = await Promise.all([
84
- loadRows({ source: "public.members" }),
85
- loadRows({ source: "public.deals", limit: 1 }),
86
- ]);
87
- return <GettingStarted invited={members.length > 1} hasDeal={deals.length > 0} />;
83
+ export default function Home() {
84
+ const members = useCount({ source: "members" });
85
+ const deals = useCount({ source: "deals" });
86
+ return <GettingStarted loading={members.loading || deals.loading} invited={members.count > 1} hasDeal={deals.count > 0} />;
88
87
  }
89
88
  ```
90
89
 
@@ -93,10 +92,11 @@ export default async function Home() {
93
92
  "use client";
94
93
  import { SetupChecklist } from "./blocks/setup-checklist";
95
94
 
96
- export default function GettingStarted({ invited, hasDeal, onHide }) {
95
+ export default function GettingStarted({ loading, invited, hasDeal, onHide }) {
97
96
  return (
98
97
  <SetupChecklist
99
98
  title="Get started"
99
+ loading={loading}
100
100
  steps={[
101
101
  { key: "invite", title: "Invite your team", description: "Bring in the people who own accounts.", actionLabel: "Invite", href: "/team", done: invited },
102
102
  { key: "deal", title: "Add your first deal", description: "One deal is enough to see the pipeline work.", actionLabel: "Add a deal", href: "/deals?new=1", done: hasDeal },
@@ -107,6 +107,8 @@ export default function GettingStarted({ invited, hasDeal, onHide }) {
107
107
  }
108
108
  ```
109
109
 
110
+ In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
111
+
110
112
  In order, with buttons that do the work in place:
111
113
 
112
114
  ```jsx
@@ -115,11 +117,7 @@ In order, with buttons that do the work in place:
115
117
  sequential
116
118
  title="Quick start"
117
119
  steps={steps}
118
- onAction={async (step) => {
119
- const res = await runSetupStep(step.key); // your server action, { ok, error }
120
- if (res.ok) router.refresh(); // the step comes back done
121
- return res;
122
- }}
120
+ onAction={(step) => runAction({ name: "run_setup_step", args: { key: step.key } })} // an action named in references/actions.md; its ok re-reads every hook, so the step comes back done
123
121
  />
124
122
  ```
125
123
 
@@ -89,7 +89,7 @@ export function ShareProject({ project, members, invite, setRole, remove }) {
89
89
  onClose={() => setOpen(false)}
90
90
  title={`Share “${project.name}”`}
91
91
  people={members}
92
- onInvite={(emails, role) => invite(project.id, emails, role)} // server actions returning { ok, error }
92
+ onInvite={(emails, role) => invite(project.id, emails, role)} // each answers { ok } or { ok: false, error }, as lib/actions' writes do
93
93
  onRoleChange={(person, role) => setRole(project.id, person.id, role)}
94
94
  onRemove={(person) => remove(project.id, person.id)}
95
95
  link={`https://app.example.com/p/${project.id}`}