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.
- package/README.md +56 -9
- package/package.json +1 -1
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +6 -6
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +10 -4
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +126 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +3 -3
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +24 -12
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chart/cartesian.jsx +1 -1
- package/registry/blocks/chart/core.jsx +33 -23
- package/registry/blocks/chart/pie.jsx +1 -1
- package/registry/blocks/chat/README.md +47 -14
- package/registry/blocks/chat/demo.jsx +2 -2
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +30 -12
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +7 -4
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +20 -7
- package/registry/blocks/org-chart/README.md +14 -9
- package/registry/blocks/page-builder/README.md +14 -7
- package/registry/blocks/page-builder/demo.jsx +3 -3
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +26 -21
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/sidebar/README.md +23 -13
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +25 -10
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +34 -22
- package/src/cli.mjs +33 -6
- package/src/install.mjs +285 -26
- package/src/registry.mjs +109 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +1100 -89
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/app/shell.jsx +54 -37
- package/templates/next/files/check/run.mjs +1514 -45
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/layout.jsx +307 -0
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +170 -51
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +15 -1
- package/templates/next/files/lib/data.js +119 -29
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/lib/hooks.js +71 -2
- package/templates/next/files/lib/platform.js +93 -4
- package/templates/next/files/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +18 -3
- 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
|
|
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) => ({ "&": "&", "<": "<", ">": ">", '"': """ })[c]);
|
|
92
92
|
|
|
93
93
|
/** A standalone HTML document for the page — for a download, an email
|
|
94
|
-
* preview, a
|
|
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)"
|
|
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 {
|
|
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 = "
|
|
141
|
+
const source = "highlights";
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
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={
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
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-
|
|
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
|
|
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 —
|
|
134
|
-
|
|
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
|
|
138
|
-
const
|
|
139
|
-
|
|
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: "
|
|
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: "
|
|
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 —
|
|
88
|
-
|
|
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
|
|
92
|
-
const { rows, error } =
|
|
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={
|
|
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: "
|
|
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: "
|
|
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
|
-
//
|
|
130
|
+
// surfaces/product.jsx — client: one product, edited
|
|
131
131
|
"use client";
|
|
132
|
-
import {
|
|
133
|
-
import {
|
|
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(
|
|
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
|
-
|
|
150
|
-
|
|
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: "
|
|
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
|
-
//
|
|
76
|
+
// surfaces/deal-notes.jsx — client: one deal's notes
|
|
77
77
|
"use client";
|
|
78
|
-
import {
|
|
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 "
|
|
81
|
-
import { docToText } from "
|
|
82
|
+
import { RecordNotes } from "../components/blocks/record-notes";
|
|
83
|
+
import { docToText } from "../components/blocks/doc-editor";
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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 {
|
|
77
|
-
import {
|
|
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(
|
|
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={
|
|
91
|
-
onSelect={(r) => router.push(
|
|
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: "
|
|
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
|
-
//
|
|
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 "
|
|
102
|
-
|
|
103
|
-
export default function Preferences(
|
|
104
|
-
const rows =
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
label: "
|
|
110
|
-
|
|
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
|
-
|
|
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: "
|
|
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: "
|
|
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 —
|
|
79
|
-
|
|
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
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
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={
|
|
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)} //
|
|
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}`}
|