makersclaw 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -8
- 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 +3 -3
- 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 +3 -3
- 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 +121 -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 +2 -2
- 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 +14 -5
- 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/chat/README.md +43 -13
- 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 +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- 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 +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- 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 +22 -20
- 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/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- 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 +19 -13
- package/src/cli.mjs +31 -4
- package/src/install.mjs +263 -21
- package/src/registry.mjs +78 -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 +810 -79
- package/templates/next/files/app/frame.js +128 -4
- package/templates/next/files/check/run.mjs +1438 -41
- package/templates/next/files/components/ui/item.jsx +2 -2
- 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 +11 -0
- 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/next.config.mjs +36 -2
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/template.json +9 -3
- package/templates/next/files/schema.sql +0 -22
|
@@ -79,7 +79,7 @@ for a file) are exported too, for your own file rows.
|
|
|
79
79
|
## Example
|
|
80
80
|
|
|
81
81
|
```jsx
|
|
82
|
-
// components/DealFiles.jsx — client
|
|
82
|
+
// components/DealFiles.jsx — client, in a server app
|
|
83
83
|
"use client";
|
|
84
84
|
import { FileUpload } from "./blocks/file-upload";
|
|
85
85
|
|
|
@@ -111,6 +111,12 @@ export default function DealFiles({ deal, onSaveFiles }) {
|
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
A server app's only. `put` needs a route of the app's own to take the
|
|
115
|
+
file (`/api/files` here), and a browser app has none: the data door
|
|
116
|
+
takes no files until uploads have a home. In a browser app, leave
|
|
117
|
+
`onUpload` out and use the block as a picker: read the file in the
|
|
118
|
+
browser, as csv-import does.
|
|
119
|
+
|
|
114
120
|
One spreadsheet to import, and a profile picture:
|
|
115
121
|
|
|
116
122
|
```jsx
|
|
@@ -364,19 +364,28 @@ one.
|
|
|
364
364
|
## Example
|
|
365
365
|
|
|
366
366
|
```jsx
|
|
367
|
-
//
|
|
367
|
+
// surfaces/launch.jsx — client: one project's board
|
|
368
368
|
"use client";
|
|
369
369
|
import { useState } from "react";
|
|
370
|
+
import { useSearchParams } from "next/navigation";
|
|
371
|
+
import { useRow } from "../lib/data";
|
|
370
372
|
import { updateRow } from "../lib/actions";
|
|
371
|
-
import { FlowCanvas } from "
|
|
373
|
+
import { FlowCanvas } from "../components/blocks/flow-canvas";
|
|
372
374
|
|
|
373
|
-
export default function
|
|
374
|
-
const
|
|
375
|
+
export default function LaunchSurface() {
|
|
376
|
+
const id = useSearchParams().get("row");
|
|
377
|
+
const { row: project, loading } = useRow({ source: "projects", id });
|
|
378
|
+
if (loading || !project) return null;
|
|
379
|
+
return <LaunchBoard key={project.id} project={project} />;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function LaunchBoard({ project }) {
|
|
383
|
+
const [board, setBoard] = useState(project.board); // jsonb, may be null; read once per project
|
|
375
384
|
return (
|
|
376
385
|
<FlowCanvas
|
|
377
386
|
value={board ?? undefined}
|
|
378
387
|
onChange={setBoard}
|
|
379
|
-
onSave={(b) => updateRow({ source: "
|
|
388
|
+
onSave={(b) => updateRow({ source: "projects", id: project.id, patch: { board: b } })}
|
|
380
389
|
label={`${project.name} board`}
|
|
381
390
|
height="calc(100dvh - 8rem)"
|
|
382
391
|
/>
|
|
@@ -384,6 +393,8 @@ export default function LaunchBoard({ project }) {
|
|
|
384
393
|
}
|
|
385
394
|
```
|
|
386
395
|
|
|
396
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
397
|
+
|
|
387
398
|
A card drawn with kit parts, and an agent reading the board:
|
|
388
399
|
|
|
389
400
|
```jsx
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// FlowCanvas — a Miro-like board on React Flow, in the workspace theme,
|
|
4
4
|
// behind one set of props (README.md beside this file).
|
|
5
5
|
//
|
|
6
|
-
// Client only, one
|
|
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 {
|
|
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:
|
|
105
|
+
onSubscribe: (email) => insertRow({ source: "subscribers", values: { email } }), // { ok, id } or { ok: false, error }
|
|
106
106
|
}}
|
|
107
107
|
legal="© 2026 Fieldline Ltd. All rights reserved."
|
|
108
108
|
legalLinks={[{ label: "Privacy", href: "/privacy" }, { label: "Terms", href: "/terms" }]}
|
|
@@ -111,6 +111,8 @@ export default function SiteFooter() {
|
|
|
111
111
|
}
|
|
112
112
|
```
|
|
113
113
|
|
|
114
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
115
|
+
|
|
114
116
|
## What it will not do
|
|
115
117
|
|
|
116
118
|
- Change the language or currency by itself: `locales[].onChange` is
|
|
@@ -145,15 +145,19 @@ drag saves, as `[{ id, patch }]`), `afterBlockersPatch(task, tasks)`,
|
|
|
145
145
|
## Example
|
|
146
146
|
|
|
147
147
|
```jsx
|
|
148
|
-
// surfaces/plan.jsx —
|
|
149
|
-
|
|
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
|
|
153
|
-
const
|
|
154
|
-
const people =
|
|
155
|
-
const projects =
|
|
156
|
-
return
|
|
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: "
|
|
175
|
-
: w.op === "update" ? await updateRow({ source: "
|
|
176
|
-
: await deleteRow({ source: "
|
|
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
|
|
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
|
|
107
|
-
const
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
14
|
+
// keeps the app one bundle.
|
|
15
15
|
// WORKER_URL maplibre-gl 6 draws tiles in a Web Worker that it loads
|
|
16
16
|
// from a file beside its own module (import.meta.url). Inside
|
|
17
17
|
// an app bundle that URL is a build-time file:// path, so the
|
|
@@ -162,20 +162,29 @@ one as it was.
|
|
|
162
162
|
## Example
|
|
163
163
|
|
|
164
164
|
```jsx
|
|
165
|
-
//
|
|
165
|
+
// surfaces/plan-map.jsx — client: one plan's map
|
|
166
166
|
"use client";
|
|
167
167
|
import { useState } from "react";
|
|
168
|
+
import { useSearchParams } from "next/navigation";
|
|
169
|
+
import { useRow } from "../lib/data";
|
|
168
170
|
import { updateRow } from "../lib/actions";
|
|
169
|
-
import { MindMap, mindMapToText } from "
|
|
171
|
+
import { MindMap, mindMapToText } from "../components/blocks/mind-map";
|
|
170
172
|
|
|
171
|
-
export default function
|
|
172
|
-
const
|
|
173
|
+
export default function PlanMapSurface() {
|
|
174
|
+
const id = useSearchParams().get("row");
|
|
175
|
+
const { row: plan, loading } = useRow({ source: "plans", id });
|
|
176
|
+
if (loading || !plan) return null;
|
|
177
|
+
return <PlanMap key={plan.id} plan={plan} />;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function PlanMap({ plan }) {
|
|
181
|
+
const [map, setMap] = useState(plan.map); // jsonb, may be null; read once per plan
|
|
173
182
|
return (
|
|
174
183
|
<MindMap
|
|
175
184
|
value={map ?? undefined}
|
|
176
185
|
defaultValue={{ text: plan.name }}
|
|
177
186
|
onChange={setMap}
|
|
178
|
-
onSave={(m) => updateRow({ source: "
|
|
187
|
+
onSave={(m) => updateRow({ source: "plans", id: plan.id, patch: { map: m, map_text: mindMapToText(m) } })}
|
|
179
188
|
label={`${plan.name} map`}
|
|
180
189
|
className="h-[calc(100dvh-8rem)]"
|
|
181
190
|
/>
|
|
@@ -183,6 +192,8 @@ export default function PlanMap({ plan }) {
|
|
|
183
192
|
}
|
|
184
193
|
```
|
|
185
194
|
|
|
195
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
196
|
+
|
|
186
197
|
An agent's outline, straight onto a map:
|
|
187
198
|
|
|
188
199
|
```jsx
|
|
@@ -106,21 +106,22 @@ One row per person, as a table holds them:
|
|
|
106
106
|
## Example
|
|
107
107
|
|
|
108
108
|
```jsx
|
|
109
|
-
//
|
|
109
|
+
// surfaces/team.jsx — client: the chart, read and rearranged
|
|
110
110
|
"use client";
|
|
111
|
-
import {
|
|
111
|
+
import { useRows, whenLanded } from "../lib/data";
|
|
112
112
|
import { updateRow } from "../lib/actions";
|
|
113
|
-
import { OrgChart } from "
|
|
113
|
+
import { OrgChart } from "../components/blocks/org-chart";
|
|
114
114
|
|
|
115
|
-
export default function TeamChart(
|
|
116
|
-
const
|
|
115
|
+
export default function TeamChart() {
|
|
116
|
+
const { rows: people, loading } = useRows({ source: "employees" });
|
|
117
|
+
if (loading) return null;
|
|
117
118
|
return (
|
|
118
119
|
<OrgChart
|
|
119
120
|
people={people}
|
|
120
121
|
managerKey="manager_id"
|
|
121
122
|
onChangeManager={async (person, managerId) => {
|
|
122
|
-
const res = await updateRow({ source: "
|
|
123
|
-
if (res.ok)
|
|
123
|
+
const res = await updateRow({ source: "employees", id: person.id, patch: { manager_id: managerId } });
|
|
124
|
+
if (res.ok) await whenLanded(); // the people are read again: the chart draws the new manager
|
|
124
125
|
return res;
|
|
125
126
|
}}
|
|
126
127
|
className="h-[calc(100dvh-8rem)]"
|
|
@@ -129,6 +130,8 @@ export default function TeamChart({ employees }) {
|
|
|
129
130
|
}
|
|
130
131
|
```
|
|
131
132
|
|
|
133
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
134
|
+
|
|
132
135
|
## What it will not do
|
|
133
136
|
|
|
134
137
|
- Show a person under two managers (dotted lines, matrix orgs): one
|
|
@@ -83,7 +83,7 @@ variables the app's Tailwind utilities read. The built-in blocks' CSS
|
|
|
83
83
|
workspace theme. Tailwind emits a theme variable only when some source
|
|
84
84
|
names it, so this block names every token a page may use
|
|
85
85
|
(`KEEP_TOKENS`). Installing the block is enough.
|
|
86
|
-
- **Outside the app** (a download, a
|
|
86
|
+
- **Outside the app** (a download, a plain file host),
|
|
87
87
|
`standaloneDocument(page, { root })` returns a full HTML document with
|
|
88
88
|
the tokens resolved and inlined. `pageDocument(page, { tokens })` is the
|
|
89
89
|
same function for the server, if you pass it the `:root{…}` string
|
|
@@ -208,31 +208,38 @@ the helpers the panel uses.
|
|
|
208
208
|
## Example
|
|
209
209
|
|
|
210
210
|
```jsx
|
|
211
|
+
// surfaces/landing.jsx — client: edit one page
|
|
211
212
|
"use client";
|
|
212
|
-
import {
|
|
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(
|
|
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
|
|
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
|
-
//
|
|
235
|
+
// The finished page, for a surface of its own that reads the row with the same useRow:
|
|
231
236
|
export function LandingPage({ row }) {
|
|
232
237
|
return <PageView page={row} label={row.title} />;
|
|
233
238
|
}
|
|
234
239
|
```
|
|
235
240
|
|
|
241
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
242
|
+
|
|
236
243
|
## What it will not do
|
|
237
244
|
|
|
238
245
|
- No live multiplayer. Two people editing the same page is last save wins.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// GrapesJS runs with <Canvas> from @grapesjs/react, so it creates none of
|
|
13
13
|
// its own panels; theme.js repaints the few things it still draws.
|
|
14
14
|
//
|
|
15
|
-
// Client only, one
|
|
15
|
+
// Client only, one bundle: the modules load with the page (they are
|
|
16
16
|
// safe on the server — see grapes.js); the editor is created in an effect
|
|
17
17
|
// after the mount guard. Until then — on the server and in the first client
|
|
18
18
|
// paint — the same frame shows with a skeleton.
|
|
@@ -27,7 +27,7 @@ export function PageView({ page, label, className }) {
|
|
|
27
27
|
|
|
28
28
|
/** Browser only: the page as a standalone HTML document with the theme
|
|
29
29
|
* resolved where `root` sits (colours, faces, radius) — for a download or
|
|
30
|
-
* a
|
|
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 = "" } = {}) {
|
|
@@ -128,40 +128,42 @@ cancels.
|
|
|
128
128
|
## Example
|
|
129
129
|
|
|
130
130
|
```jsx
|
|
131
|
+
// surfaces/contract.jsx — client: one document and its highlights
|
|
131
132
|
"use client";
|
|
132
|
-
import {
|
|
133
|
+
import { useSearchParams } from "next/navigation";
|
|
134
|
+
import { useRow, useRows, whenLanded } from "../lib/data";
|
|
135
|
+
import { deleteRow, insertRow, updateRow } from "../lib/actions";
|
|
133
136
|
import { PdfViewer } from "../components/blocks/pdf-viewer";
|
|
134
|
-
import { insertRow, updateRow, deleteRow } from "../lib/actions";
|
|
135
137
|
|
|
136
|
-
const source = "
|
|
138
|
+
const source = "highlights";
|
|
137
139
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
// A write answers once the highlights are read again, so the viewer never
|
|
141
|
+
// shows the list from before it.
|
|
142
|
+
async function landed(res) {
|
|
143
|
+
if (res.ok) await whenLanded();
|
|
144
|
+
return res;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export default function ContractReview() {
|
|
148
|
+
const id = useSearchParams().get("row");
|
|
149
|
+
const { row: doc, loading } = useRow({ source: "documents", id });
|
|
150
|
+
const { rows: highlights } = useRows({ source, filter: { document_id: id } });
|
|
151
|
+
if (loading || !doc) return null;
|
|
140
152
|
return (
|
|
141
153
|
<PdfViewer
|
|
142
154
|
file={doc.url}
|
|
143
155
|
title={doc.title}
|
|
144
156
|
highlights={highlights}
|
|
145
|
-
onHighlight={
|
|
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
|
-
}}
|
|
157
|
+
onHighlight={(h) => insertRow({ source, values: { document_id: doc.id, ...h } }).then(landed)}
|
|
158
|
+
onHighlightChange={(hid, patch) => updateRow({ source, id: hid, patch }).then(landed)}
|
|
159
|
+
onHighlightDelete={(hid) => deleteRow({ source, id: hid }).then(landed)}
|
|
160
160
|
/>
|
|
161
161
|
);
|
|
162
162
|
}
|
|
163
163
|
```
|
|
164
164
|
|
|
165
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
166
|
+
|
|
165
167
|
Preview a PDF you make (the `pdf-document` block):
|
|
166
168
|
|
|
167
169
|
```jsx
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// callbacks. pdf.js (via react-pdf, see ./pdfjs.js) draws the pages; this
|
|
7
7
|
// file is the viewer around them, in the kit and the workspace's tokens.
|
|
8
8
|
//
|
|
9
|
-
// Client only, one
|
|
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
|
],
|