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
|
@@ -157,22 +157,46 @@ const getSlots = useCallback(({ from, to }) =>
|
|
|
157
157
|
## Example
|
|
158
158
|
|
|
159
159
|
```jsx
|
|
160
|
+
// surfaces/schedule.jsx — client: the tasks on a calendar
|
|
160
161
|
"use client";
|
|
161
162
|
import { useState } from "react";
|
|
162
|
-
import {
|
|
163
|
-
import {
|
|
163
|
+
import { useRows } from "../lib/data";
|
|
164
|
+
import { deleteRow, insertRow, updateRow } from "../lib/actions";
|
|
165
|
+
import { Sheet } from "../components/ui";
|
|
166
|
+
import { CalendarView } from "../components/blocks/calendar-view";
|
|
167
|
+
import { changeWrites } from "../components/blocks/task-model";
|
|
168
|
+
|
|
169
|
+
const COLUMNS = ["id", "title", "status", "assignees", "start", "due", "project"];
|
|
170
|
+
|
|
171
|
+
// One change of the calendar's, as the rows it writes (task-list's README has the same).
|
|
172
|
+
async function save(change) {
|
|
173
|
+
for (const w of changeWrites(change, { columns: COLUMNS, keepNewIds: false })) {
|
|
174
|
+
const r =
|
|
175
|
+
w.op === "insert" ? await insertRow({ source: "tasks", values: w.values, detail: w.values.title })
|
|
176
|
+
: w.op === "update" ? await updateRow({ source: "tasks", id: w.id, patch: w.patch })
|
|
177
|
+
: await deleteRow({ source: "tasks", id: w.id });
|
|
178
|
+
if (r?.ok === false) return r;
|
|
179
|
+
}
|
|
180
|
+
return { ok: true };
|
|
181
|
+
}
|
|
164
182
|
|
|
165
|
-
export default function Schedule(
|
|
183
|
+
export default function Schedule() {
|
|
184
|
+
const tasks = useRows({ source: "tasks", orderBy: "start asc" });
|
|
185
|
+
const people = useRows({ source: "people" });
|
|
186
|
+
const projects = useRows({ source: "projects" });
|
|
166
187
|
const [open, setOpen] = useState(null);
|
|
167
188
|
return (
|
|
168
189
|
<>
|
|
169
|
-
<CalendarView tasks={tasks} people={people} projects={projects} onChange={
|
|
190
|
+
<CalendarView tasks={tasks.rows} people={people.rows} projects={projects.rows} onChange={save} onOpen={setOpen} defaultMode="week"
|
|
191
|
+
loading={tasks.loading || people.loading || projects.loading} error={tasks.error} />
|
|
170
192
|
<Sheet open={Boolean(open)} onClose={() => setOpen(null)} title={open?.title}>…</Sheet>
|
|
171
193
|
</>
|
|
172
194
|
);
|
|
173
195
|
}
|
|
174
196
|
```
|
|
175
197
|
|
|
198
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
199
|
+
|
|
176
200
|
## What it will not do
|
|
177
201
|
|
|
178
202
|
- Recurring events, time zones, or events that are not tasks: a repeat
|
|
@@ -94,33 +94,35 @@ The checkout step:
|
|
|
94
94
|
## Example
|
|
95
95
|
|
|
96
96
|
```jsx
|
|
97
|
-
//
|
|
97
|
+
// surfaces/basket.jsx — client: the cart, read and changed
|
|
98
98
|
"use client";
|
|
99
99
|
import { useState } from "react";
|
|
100
|
+
import { useRows } from "../lib/data";
|
|
100
101
|
import { deleteRow, updateRow } from "../lib/actions";
|
|
101
|
-
import { Cart } from "
|
|
102
|
+
import { Cart } from "../components/blocks/cart";
|
|
102
103
|
|
|
103
|
-
export default function Basket(
|
|
104
|
+
export default function Basket() {
|
|
105
|
+
const { rows: items, error, loading } = useRows({ source: "cart_items", orderBy: "created_at asc" });
|
|
104
106
|
const [step, setStep] = useState("cart");
|
|
105
107
|
return (
|
|
106
108
|
<Cart
|
|
107
109
|
items={items}
|
|
110
|
+
loading={loading}
|
|
108
111
|
error={error}
|
|
109
112
|
step={step}
|
|
110
113
|
shipping={0}
|
|
111
114
|
tax="Calculated at payment"
|
|
112
115
|
note="Free delivery on every order this month."
|
|
113
|
-
onQuantityChange={(item, quantity) => updateRow({ source: "
|
|
114
|
-
onRemove={(item) => deleteRow({ source: "
|
|
116
|
+
onQuantityChange={(item, quantity) => updateRow({ source: "cart_items", id: item.id, patch: { quantity } })}
|
|
117
|
+
onRemove={(item) => deleteRow({ source: "cart_items", id: item.id })}
|
|
115
118
|
onCheckout={() => setStep("checkout")}
|
|
116
119
|
onBack={() => setStep("cart")}
|
|
117
120
|
continueHref="/shop"
|
|
118
121
|
terms="I agree to the terms of sale"
|
|
119
|
-
onPay={async (
|
|
120
|
-
|
|
121
|
-
if (!
|
|
122
|
-
|
|
123
|
-
window.Paddle?.Checkout.open({ transactionId });
|
|
122
|
+
onPay={async () => {
|
|
123
|
+
// Paddle.js opens its checkout in the browser from price ids: no route of the app's own.
|
|
124
|
+
if (!window.Paddle) return { ok: false, error: "Payments have not loaded. Try again." };
|
|
125
|
+
window.Paddle.Checkout.open({ items: items.map((i) => ({ priceId: i.price_id, quantity: i.quantity })) });
|
|
124
126
|
return { ok: true };
|
|
125
127
|
}}
|
|
126
128
|
/>
|
|
@@ -128,6 +130,8 @@ export default function Basket({ items, error }) {
|
|
|
128
130
|
}
|
|
129
131
|
```
|
|
130
132
|
|
|
133
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
134
|
+
|
|
131
135
|
## What it will not do
|
|
132
136
|
|
|
133
137
|
- Take a card number, expiry or CVC. Payment is Paddle's overlay or page,
|
|
@@ -244,15 +244,20 @@ sharing one key, say).
|
|
|
244
244
|
## Example
|
|
245
245
|
|
|
246
246
|
```jsx
|
|
247
|
-
// surfaces/sales.jsx —
|
|
248
|
-
|
|
247
|
+
// surfaces/sales.jsx — client: read, hand over
|
|
248
|
+
"use client";
|
|
249
|
+
import { useRows } from "../lib/data";
|
|
249
250
|
import SalesCharts from "../components/SalesCharts";
|
|
250
251
|
|
|
251
|
-
export default
|
|
252
|
-
const
|
|
253
|
-
const
|
|
254
|
-
const
|
|
255
|
-
|
|
252
|
+
export default function SalesSurface() {
|
|
253
|
+
const months = useRows({ source: "revenue_by_month", orderBy: "month asc" });
|
|
254
|
+
const stages = useRows({ source: "deals_by_stage", orderBy: "position asc" });
|
|
255
|
+
const visits = useRows({ source: "visits_by_slot", orderBy: "position asc" });
|
|
256
|
+
if (months.loading || stages.loading || visits.loading) return null;
|
|
257
|
+
return (
|
|
258
|
+
<SalesCharts months={months.rows} stages={stages.rows} visits={visits.rows}
|
|
259
|
+
error={months.error ?? stages.error ?? visits.error} />
|
|
260
|
+
);
|
|
256
261
|
}
|
|
257
262
|
```
|
|
258
263
|
|
|
@@ -303,6 +308,8 @@ export default function SalesCharts({ months, stages, visits, error }) {
|
|
|
303
308
|
}
|
|
304
309
|
```
|
|
305
310
|
|
|
311
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
312
|
+
|
|
306
313
|
## What it will not do
|
|
307
314
|
|
|
308
315
|
- Sort, group or sum: the rows come shaped. Totals by month or by stage
|
|
@@ -244,32 +244,42 @@ export function TooltipBox({ at, title, rows, footer }) {
|
|
|
244
244
|
}
|
|
245
245
|
|
|
246
246
|
/** The numbers behind the picture, for a screen reader: a real table,
|
|
247
|
-
* visually hidden. `head` the column names, `body` rows of strings.
|
|
247
|
+
* visually hidden. `head` the column names, `body` rows of strings.
|
|
248
|
+
*
|
|
249
|
+
* The hiding is a div's, never the table's: a table takes `width: 1px`
|
|
250
|
+
* as a least width and grows to its rows, and a browser need not clip a
|
|
251
|
+
* table's overflow, so `sr-only` on the table itself left a box as wide
|
|
252
|
+
* as its longest row, which pushed a phone's page past the window (the
|
|
253
|
+
* store's probe at 375px, 2026-10-08). The div is 1px, clips what it
|
|
254
|
+
* holds and is absolute inside the chart's own box (each chart's root
|
|
255
|
+
* is `relative`); the table in it is a plain table, read as one. */
|
|
248
256
|
export function DataTable({ caption, head, body }) {
|
|
249
257
|
return (
|
|
250
|
-
<
|
|
251
|
-
<
|
|
252
|
-
|
|
253
|
-
<
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
</thead>
|
|
259
|
-
<tbody>
|
|
260
|
-
{body.map((cells, r) => (
|
|
261
|
-
<tr key={r}>
|
|
262
|
-
{cells.map((c, i) =>
|
|
263
|
-
i === 0 ? (
|
|
264
|
-
<th key={i} scope="row">{c}</th>
|
|
265
|
-
) : (
|
|
266
|
-
<td key={i}>{c}</td>
|
|
267
|
-
),
|
|
268
|
-
)}
|
|
258
|
+
<div className="sr-only">
|
|
259
|
+
<table>
|
|
260
|
+
<caption>{caption}</caption>
|
|
261
|
+
<thead>
|
|
262
|
+
<tr>
|
|
263
|
+
{head.map((h, i) => (
|
|
264
|
+
<th key={i} scope="col">{h}</th>
|
|
265
|
+
))}
|
|
269
266
|
</tr>
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
267
|
+
</thead>
|
|
268
|
+
<tbody>
|
|
269
|
+
{body.map((cells, r) => (
|
|
270
|
+
<tr key={r}>
|
|
271
|
+
{cells.map((c, i) =>
|
|
272
|
+
i === 0 ? (
|
|
273
|
+
<th key={i} scope="row">{c}</th>
|
|
274
|
+
) : (
|
|
275
|
+
<td key={i}>{c}</td>
|
|
276
|
+
),
|
|
277
|
+
)}
|
|
278
|
+
</tr>
|
|
279
|
+
))}
|
|
280
|
+
</tbody>
|
|
281
|
+
</table>
|
|
282
|
+
</div>
|
|
273
283
|
);
|
|
274
284
|
}
|
|
275
285
|
|
|
@@ -168,7 +168,7 @@ export function PieChart({
|
|
|
168
168
|
: { value: format(total), label: totalLabel };
|
|
169
169
|
|
|
170
170
|
return (
|
|
171
|
-
<div className={cx("grid min-w-0 gap-3", className)}>
|
|
171
|
+
<div className={cx("relative grid min-w-0 gap-3", className)}>
|
|
172
172
|
<div
|
|
173
173
|
ref={boxRef}
|
|
174
174
|
tabIndex={0}
|
|
@@ -127,7 +127,10 @@ not run.
|
|
|
127
127
|
has no `author`.
|
|
128
128
|
- `emptyTitle` · `emptyBody` · `emptyState` — what an empty thread says;
|
|
129
129
|
`emptyState` replaces the whole thing.
|
|
130
|
-
- `height` — default `"min(40rem, 80dvh)"
|
|
130
|
+
- `height` — default `"min(40rem, 80dvh)"` (the block's own); the list
|
|
131
|
+
scrolls inside it. Yours is a length (`"40rem"`) or `"100%"` in a sized
|
|
132
|
+
box, such as the kit's `AppLayout contentMode="fill"`: a `vh` length in
|
|
133
|
+
your code is refused by `npm run check`.
|
|
131
134
|
- `label` — what the thread is called to a screen reader.
|
|
132
135
|
- `className` — layout around it (width, margin). Never colour.
|
|
133
136
|
|
|
@@ -214,29 +217,53 @@ and the hint in its bar, Send at the end. The picker leaves out files
|
|
|
214
217
|
## Example
|
|
215
218
|
|
|
216
219
|
```jsx
|
|
217
|
-
//
|
|
220
|
+
// surfaces/ticket.jsx — client: one ticket's conversation
|
|
218
221
|
"use client";
|
|
219
222
|
import { useState } from "react";
|
|
223
|
+
import { useSearchParams } from "next/navigation";
|
|
224
|
+
import { loadRows, useRow, useRows, whenLanded } from "../lib/data";
|
|
220
225
|
import { insertRow } from "../lib/actions";
|
|
221
|
-
import { ChatThread } from "
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
import { ChatThread } from "../components/blocks/chat";
|
|
227
|
+
|
|
228
|
+
const source = "ticket_messages";
|
|
229
|
+
const PAGE = 200;
|
|
230
|
+
const oldestFirst = (rows) => [...rows].reverse();
|
|
231
|
+
const asMessage = (r) => ({ id: r.id, role: r.from_agent ? "assistant" : "user", content: r.body, createdAt: r.created_at });
|
|
232
|
+
|
|
233
|
+
export default function SupportChat() {
|
|
234
|
+
const id = useSearchParams().get("row");
|
|
235
|
+
const { row: ticket } = useRow({ source: "tickets", id });
|
|
236
|
+
// The newest page, read again after every write.
|
|
237
|
+
const { rows, loading } = useRows({ source, filter: { ticket_id: id }, orderBy: "created_at desc", limit: PAGE });
|
|
238
|
+
const [earlier, setEarlier] = useState({ rows: [], more: true }); // what onLoadMore read, oldest first
|
|
239
|
+
const [sending, setSending] = useState([]); // sent, not read back yet
|
|
240
|
+
const seen = new Map([...earlier.rows, ...oldestFirst(rows)].map((r) => [r.id, r]));
|
|
241
|
+
const shown = [...seen.values()];
|
|
242
|
+
|
|
243
|
+
async function loadMore() {
|
|
244
|
+
const oldest = shown[0]?.created_at;
|
|
245
|
+
if (!oldest) return;
|
|
246
|
+
const r = await loadRows({ source, filter: { ticket_id: id, created_at: { lt: oldest } }, orderBy: "created_at desc", limit: PAGE });
|
|
247
|
+
// What is on screen is kept too: new messages push the oldest out of the page above.
|
|
248
|
+
if (!r.error) setEarlier({ rows: [...oldestFirst(r.rows), ...shown], more: r.rows.length === PAGE });
|
|
249
|
+
}
|
|
226
250
|
|
|
227
251
|
async function send(text) {
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
252
|
+
const mid = crypto.randomUUID();
|
|
253
|
+
setSending((all) => [...all, { id: mid, role: "user", content: text, createdAt: new Date().toISOString(), status: "sending" }]);
|
|
254
|
+
const r = await insertRow({ source, values: { id: mid, ticket_id: id, body: text }, detail: text });
|
|
255
|
+
if (r.ok) await whenLanded(); // read back: it is in `rows` now
|
|
256
|
+
setSending((all) => (r.ok ? all.filter((m) => m.id !== mid) : all.map((m) => (m.id === mid ? { ...m, status: "failed", error: r.error } : m))));
|
|
233
257
|
}
|
|
234
258
|
|
|
259
|
+
if (loading || !ticket) return null;
|
|
235
260
|
return (
|
|
236
261
|
<ChatThread
|
|
237
|
-
messages={
|
|
262
|
+
messages={[...shown.map(asMessage), ...sending.filter((m) => !seen.has(m.id))]}
|
|
238
263
|
onSend={send}
|
|
239
|
-
onRetry={(m) => {
|
|
264
|
+
onRetry={(m) => { setSending((all) => all.filter((x) => x.id !== m.id)); send(m.content); }}
|
|
265
|
+
onLoadMore={loadMore}
|
|
266
|
+
hasMore={rows.length === PAGE && earlier.more}
|
|
240
267
|
assistantName={ticket.agent_name}
|
|
241
268
|
userName="You"
|
|
242
269
|
label={`Ticket ${ticket.number}`}
|
|
@@ -245,6 +272,12 @@ export default function SupportChat({ ticket, initial }) {
|
|
|
245
272
|
}
|
|
246
273
|
```
|
|
247
274
|
|
|
275
|
+
`onLoadMore` reads the page before the oldest message shown:
|
|
276
|
+
`loadRows({ source, filter: { ticket_id, created_at: { lt: oldest } } })`,
|
|
277
|
+
the same filter in a server app's `loadRows` and the door.
|
|
278
|
+
|
|
279
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
280
|
+
|
|
248
281
|
With the AI SDK in a shadcn project, `useChat()` already has the shape:
|
|
249
282
|
`<ChatThread messages={messages} onSend={(text) => sendMessage({ text })}
|
|
250
283
|
streaming={status === "streaming" || status === "submitted"} onStop={stop}
|
|
@@ -233,7 +233,7 @@ function SupportDemo() {
|
|
|
233
233
|
emptyTitle="No messages yet"
|
|
234
234
|
emptyBody="Ask about billing, your workspace or an invoice — Ada answers in a few seconds."
|
|
235
235
|
label="Support chat"
|
|
236
|
-
height="
|
|
236
|
+
height="44rem"
|
|
237
237
|
/>
|
|
238
238
|
</div>
|
|
239
239
|
);
|
|
@@ -412,7 +412,7 @@ function AssistantDemo() {
|
|
|
412
412
|
emptyTitle="What should we look at?"
|
|
413
413
|
emptyBody="Pip reads your orders, stock and returns. Pick a question or ask your own."
|
|
414
414
|
label="Planning assistant"
|
|
415
|
-
height="
|
|
415
|
+
height="40rem"
|
|
416
416
|
/>
|
|
417
417
|
</div>
|
|
418
418
|
);
|
|
@@ -193,6 +193,10 @@ export function QueryBox({ runQuery }) {
|
|
|
193
193
|
}
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
+
Running SQL a member typed is a server app's only: `runQuery` is a
|
|
197
|
+
Server Action of the app's own. A browser app has no such call, since no
|
|
198
|
+
route of the data door runs SQL a member typed.
|
|
199
|
+
|
|
196
200
|
JSON, read-only, fixed height:
|
|
197
201
|
|
|
198
202
|
```jsx
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// as you type) or plain text; line numbers, problems drawn on the text,
|
|
6
6
|
// find and replace, and a run hook on Cmd/Ctrl+Enter.
|
|
7
7
|
//
|
|
8
|
-
// Client only, one
|
|
8
|
+
// Client only, one bundle. CodeMirror needs the DOM, and the app's
|
|
9
9
|
// check refuses import() (so no next/dynamic): the modules load with the
|
|
10
10
|
// page, and the editor itself is created only after the first effect (the
|
|
11
11
|
// mount guard). Until then — on the server and in the first client paint —
|
|
@@ -79,32 +79,39 @@ The other exports, for a thread of your own:
|
|
|
79
79
|
## Example
|
|
80
80
|
|
|
81
81
|
```jsx
|
|
82
|
-
//
|
|
82
|
+
// surfaces/task.jsx — client: one task's comments
|
|
83
83
|
"use client";
|
|
84
|
-
import {
|
|
84
|
+
import { useSearchParams } from "next/navigation";
|
|
85
|
+
import { useArtifact } from "../lib/hooks";
|
|
86
|
+
import { useRows, whenLanded } from "../lib/data";
|
|
85
87
|
import { insertRow } from "../lib/actions";
|
|
86
|
-
import { CommentBox } from "
|
|
87
|
-
import { docToText } from "
|
|
88
|
-
|
|
89
|
-
export default function TaskComments(
|
|
90
|
-
const
|
|
88
|
+
import { CommentBox } from "../components/blocks/comment-box";
|
|
89
|
+
import { docToText } from "../components/blocks/doc-editor";
|
|
90
|
+
|
|
91
|
+
export default function TaskComments() {
|
|
92
|
+
const taskId = useSearchParams().get("row");
|
|
93
|
+
const { user } = useArtifact(); // the member signed in: { id, email }
|
|
94
|
+
const { rows, loading } = useRows({ source: "comments", filter: { task_id: taskId }, orderBy: "created_at asc" });
|
|
95
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
96
|
+
// created_by: a before-insert trigger sets it from the member, never the page (the app's README §5)
|
|
97
|
+
const comments = rows.map((c) => ({ id: c.id, author: c.created_by, body: c.body, createdAt: c.created_at }));
|
|
91
98
|
const findPeople = async (q) =>
|
|
92
99
|
team.filter((p) => p.name.toLowerCase().includes(q.toLowerCase())).map((p) => ({ id: p.id, label: p.name }));
|
|
93
100
|
|
|
94
101
|
async function post(doc) {
|
|
95
|
-
const res = await insertRow({
|
|
96
|
-
source: "public.comments",
|
|
97
|
-
values: { task_id: task.id, author_id: me.id, body: doc, body_text: docToText(doc) },
|
|
98
|
-
});
|
|
102
|
+
const res = await insertRow({ source: "comments", values: { task_id: taskId, body: doc, body_text: docToText(doc) } });
|
|
99
103
|
if (!res.ok) return { ok: false, error: res.error };
|
|
100
|
-
|
|
104
|
+
await whenLanded(); // the comment is read back before the box clears
|
|
101
105
|
return { ok: true };
|
|
102
106
|
}
|
|
103
107
|
|
|
104
|
-
|
|
108
|
+
if (loading) return null;
|
|
109
|
+
return <CommentBox comments={comments} onPost={post} author={user ? { name: user.email } : undefined} mentions={findPeople} />;
|
|
105
110
|
}
|
|
106
111
|
```
|
|
107
112
|
|
|
113
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
114
|
+
|
|
108
115
|
## What it will not do
|
|
109
116
|
|
|
110
117
|
- Edit or delete a posted comment, react, or reply in a nested thread: the
|
|
@@ -107,10 +107,10 @@ It is a client component: render it from a `"use client"` component.
|
|
|
107
107
|
## Example
|
|
108
108
|
|
|
109
109
|
```jsx
|
|
110
|
-
// components/Contact.jsx
|
|
110
|
+
// components/Contact.jsx — client: a message is a row
|
|
111
111
|
"use client";
|
|
112
|
+
import { insertRow } from "../lib/actions";
|
|
112
113
|
import { ContactSection } from "./blocks/contact-section";
|
|
113
|
-
import { sendContactMessage } from "../lib/actions"; // your server action
|
|
114
114
|
|
|
115
115
|
export default function Contact() {
|
|
116
116
|
return (
|
|
@@ -124,15 +124,16 @@ export default function Contact() {
|
|
|
124
124
|
]}
|
|
125
125
|
topics={{ options: ["Sales", "Support", "Partnerships"], required: true }}
|
|
126
126
|
consent="I agree to the privacy policy."
|
|
127
|
-
onSubmit={
|
|
128
|
-
const res = await sendContactMessage(values);
|
|
129
|
-
return res.ok ? { ok: true } : { ok: false, error: res.error };
|
|
130
|
-
}}
|
|
127
|
+
onSubmit={(values) => insertRow({ source: "contact_messages", values })} // { ok, id } or { ok: false, error }
|
|
131
128
|
/>
|
|
132
129
|
);
|
|
133
130
|
}
|
|
134
131
|
```
|
|
135
132
|
|
|
133
|
+
The row is the message: the app's agent, or a job, answers it.
|
|
134
|
+
|
|
135
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
136
|
+
|
|
136
137
|
## What it will not do
|
|
137
138
|
|
|
138
139
|
- Send anything: `onSubmit` is yours (save a row, email the team).
|
|
@@ -93,33 +93,55 @@ creates, fields }` (each row's `__problems` is `{ key: message }`) ·
|
|
|
93
93
|
## Example
|
|
94
94
|
|
|
95
95
|
```jsx
|
|
96
|
+
// surfaces/import.jsx — client: contacts from a spreadsheet
|
|
96
97
|
"use client";
|
|
97
98
|
import { useRouter } from "next/navigation";
|
|
98
|
-
import {
|
|
99
|
-
import {
|
|
99
|
+
import { useRows } from "../lib/data";
|
|
100
|
+
import { runAction } from "../lib/actions";
|
|
101
|
+
import { CsvImport } from "../components/blocks/csv-import";
|
|
100
102
|
|
|
101
|
-
|
|
103
|
+
const FIELDS = [
|
|
104
|
+
{ key: "name", name: "Name", type: "text", required: true },
|
|
105
|
+
{ key: "email", name: "Email", type: "text", required: true },
|
|
106
|
+
{ key: "owner", name: "Owner", type: "person" },
|
|
107
|
+
];
|
|
108
|
+
|
|
109
|
+
export default function ImportContacts() {
|
|
102
110
|
const router = useRouter();
|
|
111
|
+
const { rows: team } = useRows({ source: "people", orderBy: "name asc" });
|
|
103
112
|
return (
|
|
104
113
|
<CsvImport
|
|
105
|
-
fields={
|
|
114
|
+
fields={FIELDS}
|
|
106
115
|
people={team}
|
|
107
116
|
noun="contact"
|
|
108
117
|
onImport={async ({ rows }) => {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
imported++;
|
|
114
|
-
}
|
|
115
|
-
return { ok: true, imported };
|
|
118
|
+
// One action, one transaction: a write per row would stop halfway,
|
|
119
|
+
// and the door takes 300 writes a minute.
|
|
120
|
+
const r = await runAction({ name: "import_contacts", args: { rows: rows.map(({ id, ...values }) => values) } });
|
|
121
|
+
return r.ok ? { ok: true, imported: r.result?.imported } : r;
|
|
116
122
|
}}
|
|
117
|
-
onDone={() => router.push("/contacts")}
|
|
123
|
+
onDone={() => router.push("/s/contacts")}
|
|
118
124
|
/>
|
|
119
125
|
);
|
|
120
126
|
}
|
|
121
127
|
```
|
|
122
128
|
|
|
129
|
+
The action, named in `references/actions.md` and made with `sql`:
|
|
130
|
+
|
|
131
|
+
```sql
|
|
132
|
+
create or replace function import_contacts(args jsonb) returns jsonb
|
|
133
|
+
language sql as $$
|
|
134
|
+
with made as (
|
|
135
|
+
insert into contacts (name, email, owner)
|
|
136
|
+
select name, email, owner from jsonb_populate_recordset(null::contacts, args->'rows')
|
|
137
|
+
returning 1
|
|
138
|
+
)
|
|
139
|
+
select jsonb_build_object('imported', count(*)) from made
|
|
140
|
+
$$;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
144
|
+
|
|
123
145
|
## What it will not do
|
|
124
146
|
|
|
125
147
|
- Excel files (.xlsx) are not read: save the sheet as CSV first (the
|
|
@@ -252,8 +252,19 @@ argument, `ids`, is every selected id, including rows not on this page.
|
|
|
252
252
|
|
|
253
253
|
```jsx
|
|
254
254
|
const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSize: 25 });
|
|
255
|
-
//
|
|
256
|
-
|
|
255
|
+
// One page through the door: the filter grammar, an order, limit and offset.
|
|
256
|
+
const filter = {
|
|
257
|
+
...Object.fromEntries(Object.entries(q.filters).filter(([, values]) => values.length)),
|
|
258
|
+
customer: q.search ? { prefix: q.search } : undefined, // starts with, case ignored
|
|
259
|
+
};
|
|
260
|
+
const page = useRows({ source: "invoices", filter, orderBy: q.sort.map((s) => `${s.key} ${s.dir}`).join(", ") || undefined,
|
|
261
|
+
limit: q.pageSize, offset: (q.page - 1) * q.pageSize });
|
|
262
|
+
const total = useCount({ source: "invoices", filter });
|
|
263
|
+
const { status, ...others } = filter; // a facet counts over the other filters
|
|
264
|
+
const byStatus = useCountBy({ source: "invoices", column: "status", filter: others });
|
|
265
|
+
const statusCounts = Object.entries(byStatus.counts).map(([value, count]) => ({ value, label: value, count }));
|
|
266
|
+
|
|
267
|
+
<DataTable manual rows={page.rows} rowCount={total.count} loading={page.loading || total.loading}
|
|
257
268
|
search={q.search} onSearchChange={(search) => setQ((s) => ({ ...s, search }))}
|
|
258
269
|
filters={q.filters} onFiltersChange={(filters) => setQ((s) => ({ ...s, filters }))}
|
|
259
270
|
sort={q.sort} onSortChange={(sort) => setQ((s) => ({ ...s, sort }))}
|
|
@@ -262,6 +273,12 @@ const [q, setQ] = useState({ search: "", filters: {}, sort: [], page: 1, pageSiz
|
|
|
262
273
|
columns={COLUMNS} facets={[{ key: "status", options: statusCounts }]} />
|
|
263
274
|
```
|
|
264
275
|
|
|
276
|
+
`useRows`, `useCount` and `useCountBy` are `lib/data.js`'s in a browser
|
|
277
|
+
app. `offset` goes up to 10,000; past it, page with `cursor` (the last
|
|
278
|
+
answer's `next`, given when `orderBy` names one column). A server app's
|
|
279
|
+
own `loadRows` takes no `offset`: its `"use client"` component takes
|
|
280
|
+
these hooks from `lib/door.js`.
|
|
281
|
+
|
|
265
282
|
The pure helpers the table uses are exported too — `processRows`,
|
|
266
283
|
`searchRows`, `filterRows`, `sortRows`, `facetOptions`, `nextSort`,
|
|
267
284
|
`groupRows`, `groupValueOf` and `useControllable` — so a surface can compute the same answer the table
|
|
@@ -269,12 +286,25 @@ shows (a total over the filtered rows) without drawing it twice.
|
|
|
269
286
|
|
|
270
287
|
## Example
|
|
271
288
|
|
|
289
|
+
```jsx
|
|
290
|
+
// surfaces/invoices.jsx — client: read, hand over
|
|
291
|
+
"use client";
|
|
292
|
+
import { useRows } from "../lib/data";
|
|
293
|
+
import Invoices from "../components/Invoices";
|
|
294
|
+
|
|
295
|
+
export default function InvoicesSurface() {
|
|
296
|
+
const { rows, error, loading } = useRows({ source: "invoices", orderBy: "due asc" });
|
|
297
|
+
if (loading) return null; // rows only once read: to useFixture, a read in flight looks like an empty table
|
|
298
|
+
return <Invoices invoices={rows} error={error} />;
|
|
299
|
+
}
|
|
300
|
+
```
|
|
301
|
+
|
|
272
302
|
```jsx
|
|
273
303
|
// components/Invoices.jsx — client: the table, the row sheet
|
|
274
304
|
"use client";
|
|
275
305
|
import { useState } from "react";
|
|
276
306
|
import { updateRow } from "../lib/actions";
|
|
277
|
-
import { Badge, Button, Sheet } from "./ui";
|
|
307
|
+
import { Badge, Button, Notice, Sheet } from "./ui";
|
|
278
308
|
import { FixtureNotice, useFixture } from "./parts";
|
|
279
309
|
import { DataTable } from "./blocks/data-table";
|
|
280
310
|
|
|
@@ -283,7 +313,7 @@ const COLUMNS = [
|
|
|
283
313
|
{ key: "number", header: "Invoice", sortable: true, hideable: false },
|
|
284
314
|
{ key: "customer", header: "Customer", sortable: true },
|
|
285
315
|
{ key: "status", header: "Status", sortable: true, cell: (r) => <Badge tone={TONE[r.status]}>{r.status}</Badge> },
|
|
286
|
-
{ key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${r.amount.toLocaleString()}` },
|
|
316
|
+
{ key: "amount", header: "Amount", sortable: true, align: "end", cell: (r) => `$${Number(r.amount).toLocaleString()}` }, // numeric arrives as text
|
|
287
317
|
{ key: "due", header: "Due", sortable: true },
|
|
288
318
|
];
|
|
289
319
|
const SAMPLE = [
|
|
@@ -291,14 +321,15 @@ const SAMPLE = [
|
|
|
291
321
|
{ number: "INV-1002", customer: "Acme Ltd", status: "overdue", amount: 450, due: "2026-09-01" },
|
|
292
322
|
];
|
|
293
323
|
|
|
294
|
-
export default function Invoices({ invoices }) {
|
|
324
|
+
export default function Invoices({ invoices, error }) {
|
|
295
325
|
const fixture = useFixture(invoices, SAMPLE);
|
|
296
326
|
const [open, setOpen] = useState(null);
|
|
297
327
|
const markPaid = (rows) =>
|
|
298
|
-
Promise.all(rows.map((r) => updateRow({ source: "
|
|
328
|
+
Promise.all(rows.map((r) => updateRow({ source: "invoices", id: r.id, patch: { status: "paid" } })))
|
|
299
329
|
.then((all) => all.find((r) => r.ok === false) ?? { ok: true });
|
|
300
330
|
return (
|
|
301
331
|
<div className="grid gap-4">
|
|
332
|
+
{error ? <Notice tone="danger">{error}</Notice> : null}
|
|
302
333
|
{fixture.isFixture ? <FixtureNotice onClear={fixture.clear} /> : null}
|
|
303
334
|
<DataTable
|
|
304
335
|
label="Invoices"
|
|
@@ -323,6 +354,8 @@ export default function Invoices({ invoices }) {
|
|
|
323
354
|
}
|
|
324
355
|
```
|
|
325
356
|
|
|
357
|
+
In a server-mode app the same calls are `loadRows` in a server surface and the Server Actions of `lib/actions`.
|
|
358
|
+
|
|
326
359
|
### Details, groups and totals
|
|
327
360
|
|
|
328
361
|
A budget: line items under departments, sorted within them, a subtotal
|
|
@@ -364,8 +397,8 @@ the row and the rest in the menu:
|
|
|
364
397
|
]} />
|
|
365
398
|
```
|
|
366
399
|
|
|
367
|
-
`setStatus` and `remove` return a promise of `{ ok }`, as
|
|
368
|
-
|
|
400
|
+
`setStatus` and `remove` return a promise of `{ ok }`, as `updateRow` and
|
|
401
|
+
`deleteRow` do; a failure is said above the table.
|
|
369
402
|
|
|
370
403
|
## What it will not do
|
|
371
404
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// Diagram — Mermaid text drawn in the workspace theme, with zoom and pan.
|
|
4
4
|
//
|
|
5
|
-
// Client only, one
|
|
5
|
+
// Client only, one bundle: Mermaid needs the DOM to measure text,
|
|
6
6
|
// and the app's check refuses import() (so no next/dynamic). The modules
|
|
7
7
|
// load with the page; the drawing happens after the first effect (the
|
|
8
8
|
// mount guard). Until then — on the server and in the first client paint —
|