makersclaw 0.3.2 → 0.5.1
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 +3 -1
- package/package.json +1 -1
- package/r/annotate.json +3 -3
- package/r/approval-request.json +4 -4
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/board.json +2 -2
- package/r/calendar-view.json +9 -9
- package/r/cart.json +2 -2
- package/r/chart.json +7 -7
- package/r/chat.json +3 -3
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +5 -5
- package/r/contact-section.json +2 -2
- package/r/core.json +17 -11
- package/r/csv-import.json +3 -3
- package/r/data-table.json +4 -4
- package/r/diagram.json +2 -2
- package/r/doc-editor.json +2 -2
- package/r/doc-page.json +3 -3
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +2 -2
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +2 -2
- package/r/gantt.json +3 -3
- package/r/job-panel.json +8 -5
- package/r/json-editor.json +3 -3
- package/r/map.json +2 -2
- package/r/menubar.json +2 -2
- package/r/mind-map.json +6 -6
- 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 +2 -2
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +3 -3
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-notes.json +6 -6
- package/r/registry.json +131 -123
- package/r/search-dialog.json +2 -2
- package/r/settings-list.json +2 -2
- package/r/setup-checklist.json +2 -2
- package/r/share-dialog.json +2 -2
- package/r/sidebar.json +4 -4
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +2 -2
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +3 -3
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +2 -2
- package/r/whiteboard.json +3 -3
- package/registry/blocks/approval-request/README.md +2 -1
- package/registry/blocks/approval-request/approval-request.jsx +5 -3
- package/registry/blocks/calendar-view/README.md +8 -0
- package/registry/blocks/calendar-view/agenda.jsx +5 -4
- package/registry/blocks/calendar-view/calendar.jsx +23 -8
- package/registry/blocks/calendar-view/demo.jsx +2 -0
- package/registry/blocks/calendar-view/month.jsx +6 -5
- package/registry/blocks/calendar-view/time-grid.jsx +9 -7
- package/registry/blocks/calendar-view/tray.jsx +5 -4
- package/registry/blocks/chart/README.md +9 -4
- package/registry/blocks/chart/cartesian.jsx +31 -24
- package/registry/blocks/chart/core.jsx +21 -0
- package/registry/blocks/chart/pie.jsx +15 -10
- package/registry/blocks/chart/sparkline.jsx +32 -14
- package/registry/blocks/chat/message.jsx +4 -4
- package/registry/blocks/comment-box/README.md +1 -1
- package/registry/blocks/comment-box/comment-time.jsx +4 -6
- package/registry/blocks/data-table/README.md +4 -0
- package/registry/blocks/data-table/data-table.jsx +20 -6
- package/registry/blocks/flow-canvas/edges.jsx +4 -2
- package/registry/blocks/flow-canvas/theme.js +59 -48
- package/registry/blocks/job-panel/README.md +40 -3
- package/registry/blocks/job-panel/block.json +2 -2
- package/registry/blocks/job-panel/demo.jsx +75 -6
- package/registry/blocks/job-panel/job-panel.jsx +60 -11
- package/registry/blocks/mind-map/README.md +9 -2
- package/registry/blocks/mind-map/mind-map.jsx +51 -12
- package/registry/blocks/mind-map/tree.js +33 -0
- package/registry/blocks/org-chart/org-chart.jsx +1 -1
- package/registry/blocks/pdf-viewer/notes.jsx +4 -3
- package/registry/blocks/record-notes/README.md +1 -1
- package/registry/blocks/record-notes/record-notes.jsx +4 -5
- package/registry/blocks/sidebar/sidebar-panel.jsx +4 -4
- package/registry/blocks/sidebar/sidebar.jsx +13 -13
- package/registry/blocks/signature-pad/signature.js +8 -1
- package/registry/blocks/spreadsheet/cells.jsx +2 -2
- package/src/shadcn.mjs +1 -1
- package/templates/next/files/README.md +410 -86
- package/templates/next/files/app/auto-refresh.jsx +42 -15
- package/templates/next/files/app/claw-alive/route.js +17 -0
- package/templates/next/files/app/error-relay.jsx +9 -4
- package/templates/next/files/app/escape-relay.jsx +27 -0
- package/templates/next/files/app/frame.js +409 -0
- package/templates/next/files/app/layout.jsx +18 -3
- package/templates/next/files/check/run.mjs +51 -18
- package/templates/next/files/components/parts/fixture.jsx +29 -4
- package/templates/next/files/components/parts/index.js +2 -1
- package/templates/next/files/components/parts/live-work.jsx +235 -0
- package/templates/next/files/components/ui/button.jsx +6 -1
- package/templates/next/files/components/ui/calendar.jsx +24 -0
- package/templates/next/files/components/ui/date-picker.jsx +15 -2
- package/templates/next/files/components/ui/field.jsx +12 -3
- package/templates/next/files/components/ui/icon.jsx +37 -2
- package/templates/next/files/components/ui/index.js +1 -0
- package/templates/next/files/components/ui/sheet.jsx +38 -2
- package/templates/next/files/components/ui/toggle.jsx +20 -2
- package/templates/next/files/lib/actions.js +63 -5
- package/templates/next/files/lib/claw.js +127 -28
- package/templates/next/files/lib/data.js +4 -1
- package/templates/next/files/lib/db.js +18 -1
- package/templates/next/files/lib/hooks.js +493 -37
- package/templates/next/files/lib/platform.js +113 -15
- package/templates/next/files/surfaces/README.md +3 -0
- package/templates/next/template.json +19 -8
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { useId } from "react";
|
|
4
4
|
import { cx } from "../../ui";
|
|
5
|
-
import { defaultFormat, idFrom, num, tone, useReveal, useWidth } from "./core";
|
|
5
|
+
import { defaultFormat, idFrom, num, runs, tone, useReveal, useWidth } from "./core";
|
|
6
6
|
|
|
7
7
|
// Sparkline — a trend with no axes, the size of a line of text: beside a
|
|
8
8
|
// KPI, in a table cell. Its scale is its own low to high (not zero), so
|
|
@@ -26,14 +26,20 @@ export function Sparkline({
|
|
|
26
26
|
const [boxRef, width] = useWidth();
|
|
27
27
|
const shown = useReveal(width > 0);
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
// Every entry keeps its place along the width; a missing one is a gap —
|
|
30
|
+
// the line breaks there — never a zero. The scale is the values there are.
|
|
31
|
+
const raw = (Array.isArray(data) ? data : []).map((d) => num(typeof d === "object" && d !== null ? d[value] : d));
|
|
32
|
+
const vals = raw.filter((v) => v != null);
|
|
30
33
|
const n = vals.length;
|
|
31
34
|
const lo = n ? Math.min(...vals) : 0;
|
|
32
35
|
const hi = n ? Math.max(...vals) : 0;
|
|
33
36
|
const pad = 3;
|
|
34
|
-
const px = (i) => pad + (
|
|
37
|
+
const px = (i) => pad + (raw.length === 1 ? (width - pad * 2) / 2 : (i * (width - pad * 2)) / (raw.length - 1));
|
|
35
38
|
const py = (v) => pad + (hi === lo ? (height - pad * 2) / 2 : (1 - (v - lo) / (hi - lo)) * (height - pad * 2));
|
|
36
|
-
const
|
|
39
|
+
const segs = runs(raw.map((v, i) => (v == null ? null : [px(i), py(v)])));
|
|
40
|
+
const path = (seg) => seg.map(([x, y], i) => `${i ? "L" : "M"}${f(x)},${f(y)}`).join("");
|
|
41
|
+
const lastSeg = segs[segs.length - 1];
|
|
42
|
+
const last = lastSeg?.[lastSeg.length - 1];
|
|
37
43
|
const words = n
|
|
38
44
|
? `${label}: from ${format(vals[0])} to ${format(vals[n - 1])}, low ${format(lo)}, high ${format(hi)}.`
|
|
39
45
|
: `${label}: no data yet.`;
|
|
@@ -68,16 +74,28 @@ export function Sparkline({
|
|
|
68
74
|
</mask>
|
|
69
75
|
</defs>
|
|
70
76
|
<g clipPath={`url(#${uid}-clip)`}>
|
|
71
|
-
{area
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
{area
|
|
78
|
+
? segs.map((seg, k) =>
|
|
79
|
+
seg.length > 1 ? (
|
|
80
|
+
<path
|
|
81
|
+
key={k}
|
|
82
|
+
d={`${path(seg)}L${f(seg[seg.length - 1][0])},${height}L${f(seg[0][0])},${height}Z`}
|
|
83
|
+
className={tone(t, "fill")}
|
|
84
|
+
fillOpacity={0.3}
|
|
85
|
+
mask={`url(#${uid}-fade)`}
|
|
86
|
+
/>
|
|
87
|
+
) : null,
|
|
88
|
+
)
|
|
89
|
+
: null}
|
|
90
|
+
{segs.map((seg, k) =>
|
|
91
|
+
seg.length > 1 ? (
|
|
92
|
+
<path key={k} d={path(seg)} fill="none" className={tone(t, "stroke")} strokeWidth={1.75} strokeLinejoin="round" strokeLinecap="round" />
|
|
93
|
+
) : dot && seg === lastSeg ? null : (
|
|
94
|
+
// A value alone between two gaps: a dot the line's width.
|
|
95
|
+
<circle key={k} cx={seg[0][0]} cy={seg[0][1]} r={1.75} className={tone(t, "fill")} />
|
|
96
|
+
),
|
|
97
|
+
)}
|
|
98
|
+
{dot ? <circle cx={last[0]} cy={last[1]} r={2.5} className={tone(t, "fill")} /> : null}
|
|
81
99
|
</g>
|
|
82
100
|
</>
|
|
83
101
|
)}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useId, useRef, useState } from "react";
|
|
4
|
-
import { Avatar, Button, Collapsible, IconButton, Icon, Menu, Spinner, Textarea, Toggle, Tooltip, cx } from "../../ui";
|
|
4
|
+
import { Avatar, Button, Collapsible, IconButton, Icon, Menu, Spinner, Textarea, Toggle, Tooltip, cx, toLocalDate } from "../../ui";
|
|
5
5
|
import { Attachment } from "./attachment";
|
|
6
6
|
import { ChatText } from "./text";
|
|
7
7
|
|
|
@@ -25,10 +25,10 @@ export function textOf(message) {
|
|
|
25
25
|
.join("");
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/** A Date, an ISO time or a day alone ("2026-09-28": that day's local
|
|
29
|
+
* midnight, never UTC's) → a Date; null when it is none. */
|
|
28
30
|
export function toDate(value) {
|
|
29
|
-
|
|
30
|
-
const d = value instanceof Date ? value : new Date(value);
|
|
31
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
31
|
+
return toLocalDate(value);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export function formatTime(date) {
|
|
@@ -30,7 +30,7 @@ A comment in `comments`:
|
|
|
30
30
|
id: "c-81",
|
|
31
31
|
author: { id: "u-lena", name: "Lena Fischer", src: "https://…/lena.jpg" }, // or just "Lena Fischer"
|
|
32
32
|
body: [ /* the doc onPost gave you */ ], // or a plain string
|
|
33
|
-
createdAt: "2026-09-23T09:12:00Z", // ISO string, Date or
|
|
33
|
+
createdAt: "2026-09-23T09:12:00Z", // ISO string, Date, ms, or a day ("2026-09-23": that day, never UTC's)
|
|
34
34
|
edited: false, // true adds "(edited)"
|
|
35
35
|
}
|
|
36
36
|
```
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// tick (not a timer per comment) keeps every label current. The full date
|
|
9
9
|
// is always in the title.
|
|
10
10
|
import { useEffect, useState } from "react";
|
|
11
|
-
import { cx } from "../../ui";
|
|
11
|
+
import { cx, toLocalDate } from "../../ui";
|
|
12
12
|
|
|
13
13
|
const listeners = new Set();
|
|
14
14
|
let timer = null;
|
|
@@ -34,11 +34,9 @@ export function useNow() {
|
|
|
34
34
|
return now;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return Number.isNaN(d.getTime()) ? null : d;
|
|
41
|
-
};
|
|
37
|
+
/** A Date, an ISO time or a day alone ("2026-09-28", a `date` column: that
|
|
38
|
+
* day's local midnight, never UTC's) → a Date; null when it is none. */
|
|
39
|
+
export const toDate = (v) => toLocalDate(v);
|
|
42
40
|
|
|
43
41
|
const fmt = (opts) => new Intl.DateTimeFormat(undefined, opts);
|
|
44
42
|
const TIME = { hour: "numeric", minute: "2-digit" };
|
|
@@ -70,6 +70,10 @@ one can carry its own (report-table's `kind`).
|
|
|
70
70
|
- `label` — what the table is called to a screen reader, default `"Table"`.
|
|
71
71
|
- `noun` — what a row is ("invoice"): used in "1–10 of 60 invoices", the
|
|
72
72
|
facet counts and the announcements. Default `"row"`.
|
|
73
|
+
- `nounPlural` — the plural, when the regular ending is wrong for it:
|
|
74
|
+
`noun="person" nounPlural="people"`. Default: `noun` with the regular
|
|
75
|
+
English ending — "invoice" → "invoices", "company" → "companies",
|
|
76
|
+
"address" → "addresses", "batch" → "batches", "analysis" → "analyses".
|
|
73
77
|
|
|
74
78
|
Search
|
|
75
79
|
|
|
@@ -101,7 +101,19 @@ function humanize(key) {
|
|
|
101
101
|
return text ? text.charAt(0).toUpperCase() + text.slice(1) : "";
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
|
|
104
|
+
/** A noun's plural by the regular English endings: "invoice" → "invoices",
|
|
105
|
+
* "company" → "companies", "address" → "addresses", "batch" → "batches",
|
|
106
|
+
* "analysis" → "analyses". Anything else is the table's `nounPlural`
|
|
107
|
+
* ("person" → "people"). Until 0.5.1 it only added "s": "11 companys". */
|
|
108
|
+
export function pluralOf(noun) {
|
|
109
|
+
const s = String(noun ?? "");
|
|
110
|
+
if (/[^aeiou]y$/.test(s)) return `${s.slice(0, -1)}ies`;
|
|
111
|
+
if (/sis$/.test(s)) return `${s.slice(0, -2)}es`;
|
|
112
|
+
if (/(?:s|x|z|ch|sh)$/.test(s)) return `${s}es`;
|
|
113
|
+
return `${s}s`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function plural(n, one, many = pluralOf(one)) {
|
|
105
117
|
return `${n.toLocaleString()} ${n === 1 ? one : many}`;
|
|
106
118
|
}
|
|
107
119
|
|
|
@@ -186,12 +198,14 @@ export function DataTable({
|
|
|
186
198
|
emptyAction,
|
|
187
199
|
emptyIcon = "inbox",
|
|
188
200
|
noun = "row",
|
|
201
|
+
nounPlural,
|
|
189
202
|
mobile = "scroll",
|
|
190
203
|
cardTitle,
|
|
191
204
|
variant = "outline",
|
|
192
205
|
className,
|
|
193
206
|
}) {
|
|
194
207
|
const uid = useId().replace(/[^\w-]/g, "");
|
|
208
|
+
const nouns = nounPlural || pluralOf(noun);
|
|
195
209
|
const all = rows ?? NONE;
|
|
196
210
|
const cols = useMemo(() => (columns ?? []).map((c) => ({ ...c, key: String(c.key), header: c.header ?? humanize(c.key) })), [columns]);
|
|
197
211
|
const facetList = useMemo(
|
|
@@ -261,8 +275,8 @@ export function DataTable({
|
|
|
261
275
|
}, [current, pageCount, loading]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
262
276
|
|
|
263
277
|
useEffect(() => {
|
|
264
|
-
if (!loading && narrowed) setAnnounce(total === 0 ? "No matches" : `${plural(total, noun)} match`);
|
|
265
|
-
}, [total, narrowed, loading, noun]);
|
|
278
|
+
if (!loading && narrowed) setAnnounce(total === 0 ? "No matches" : `${plural(total, noun, nouns)} match`);
|
|
279
|
+
}, [total, narrowed, loading, noun, nouns]);
|
|
266
280
|
|
|
267
281
|
// ---- changes that move back to page 1 ---------------------------------
|
|
268
282
|
|
|
@@ -358,7 +372,7 @@ export function DataTable({
|
|
|
358
372
|
return;
|
|
359
373
|
}
|
|
360
374
|
if (action.keepSelection !== true) setPicked([]);
|
|
361
|
-
setAnnounce(`${action.label}: done for ${plural(real.length, noun)}`);
|
|
375
|
+
setAnnounce(`${action.label}: done for ${plural(real.length, noun, nouns)}`);
|
|
362
376
|
}
|
|
363
377
|
|
|
364
378
|
// ---- drawing -----------------------------------------------------------
|
|
@@ -588,7 +602,7 @@ export function DataTable({
|
|
|
588
602
|
<Empty
|
|
589
603
|
icon="search"
|
|
590
604
|
title="No matches"
|
|
591
|
-
body={narrowed ? "Nothing matches the search and filters." : `No ${
|
|
605
|
+
body={narrowed ? "Nothing matches the search and filters." : `No ${nouns} on this page.`}
|
|
592
606
|
action={
|
|
593
607
|
narrowed ? (
|
|
594
608
|
<Button size="sm" icon="x" onClick={clearFilters}>
|
|
@@ -1137,7 +1151,7 @@ export function DataTable({
|
|
|
1137
1151
|
{paginate && !body && (total > 0 || loading) ? (
|
|
1138
1152
|
<div className="flex flex-wrap items-center gap-x-4 gap-y-2">
|
|
1139
1153
|
<p data-nums className="w-full text-sm text-muted-foreground sm:w-auto" id={`${uid}-range`}>
|
|
1140
|
-
{loading ? "Loading…" : `${from.toLocaleString()}–${to.toLocaleString()} of ${plural(total, noun)}`}
|
|
1154
|
+
{loading ? "Loading…" : `${from.toLocaleString()}–${to.toLocaleString()} of ${plural(total, noun, nouns)}`}
|
|
1141
1155
|
</p>
|
|
1142
1156
|
<Pagination
|
|
1143
1157
|
page={pageNow}
|
|
@@ -68,8 +68,10 @@ export function FlowEdge({ id, source, target, selected, data, label }) {
|
|
|
68
68
|
const g = connectorGeometry(p1, sides.source, p2, sides.target, data?.line ?? "curved");
|
|
69
69
|
const arrow = data?.arrow ?? "end";
|
|
70
70
|
const editing = !readOnly && editingEdge === id;
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
// `\_\_` and String.raw: an underscore in [...] is a space to Tailwind
|
|
72
|
+
// (theme.js says why).
|
|
73
|
+
const ink = selected ? "stroke-primary" : String.raw`stroke-muted-foreground [.react-flow\_\_edge:hover_&]:stroke-foreground`;
|
|
74
|
+
const fill = selected ? "fill-primary" : String.raw`fill-muted-foreground [.react-flow\_\_edge:hover_&]:fill-foreground`;
|
|
73
75
|
return (
|
|
74
76
|
<>
|
|
75
77
|
<path d={g.d} fill="none" stroke="transparent" strokeWidth={18} className="react-flow__edge-interaction" />
|
|
@@ -10,71 +10,82 @@
|
|
|
10
10
|
// one thing that outranks it. Our own nodes and connectors draw with
|
|
11
11
|
// token classes directly (nodes.jsx, edges.jsx), so the library's node and
|
|
12
12
|
// edge colours are never seen.
|
|
13
|
+
//
|
|
14
|
+
// React Flow's classes are BEM, `react-flow__node`, and Tailwind reads an
|
|
15
|
+
// underscore inside [...] as a space: written plainly, every rule below
|
|
16
|
+
// compiled to `.react-flow node`, which matches nothing, and none of this
|
|
17
|
+
// theme ever reached the page — the attribution was the library's grey box
|
|
18
|
+
// with its grey text, unreadable on a dark workspace (makersclaw-store
|
|
19
|
+
// BLOCKS.md, mind-maps 0.1.0). `\_` keeps an underscore, and String.raw
|
|
20
|
+
// keeps the backslash in the class name, where the rule Tailwind builds
|
|
21
|
+
// from this file's text expects it. Every React Flow class below is
|
|
22
|
+
// written rf`…`; test/cli.test.mjs refuses a plain one in any block.
|
|
23
|
+
const rf = String.raw;
|
|
13
24
|
export const THEME = [
|
|
14
25
|
// the canvas: the page's background with dots in the muted ink
|
|
15
26
|
"[--xy-background-color:transparent]!",
|
|
16
27
|
"[--xy-background-pattern-color:var(--color-muted-foreground)]!",
|
|
17
|
-
|
|
28
|
+
rf`[&_.react-flow\_\_background]:opacity-35!`,
|
|
18
29
|
// the marquee, and the box around a multi-selection
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
rf`[&_.react-flow\_\_selection]:bg-primary/8!`,
|
|
31
|
+
rf`[&_.react-flow\_\_selection]:border!`,
|
|
32
|
+
rf`[&_.react-flow\_\_selection]:border-solid!`,
|
|
33
|
+
rf`[&_.react-flow\_\_selection]:border-primary/70!`,
|
|
34
|
+
rf`[&_.react-flow\_\_selection]:rounded-sm!`,
|
|
35
|
+
rf`[&_.react-flow\_\_nodesselection-rect]:bg-transparent!`,
|
|
36
|
+
rf`[&_.react-flow\_\_nodesselection-rect]:border!`,
|
|
37
|
+
rf`[&_.react-flow\_\_nodesselection-rect]:border-dashed!`,
|
|
38
|
+
rf`[&_.react-flow\_\_nodesselection-rect]:border-primary/60!`,
|
|
39
|
+
rf`[&_.react-flow\_\_nodesselection-rect]:rounded-sm!`,
|
|
29
40
|
// the line drawn while a connector is being dragged out
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
rf`[&_.react-flow\_\_connection-path]:stroke-primary!`,
|
|
42
|
+
rf`[&_.react-flow\_\_connection-path]:[stroke-width:2]!`,
|
|
32
43
|
// cursors that match the tool (flow-canvas.jsx sets data-tool on the root)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
rf`[&_.react-flow\_\_pane.selection]:cursor-default!`,
|
|
45
|
+
rf`[&_.react-flow\_\_node.draggable]:cursor-default!`,
|
|
46
|
+
rf`[&_.react-flow\_\_node.draggable.dragging]:cursor-grabbing!`,
|
|
47
|
+
rf`data-[tool=hand]:[&_.react-flow\_\_pane]:cursor-grab!`,
|
|
48
|
+
rf`data-[tool=hand]:[&_.react-flow\_\_pane.dragging]:cursor-grabbing!`,
|
|
49
|
+
rf`data-[tool=hand]:[&_.react-flow\_\_node]:cursor-grab!`,
|
|
50
|
+
rf`data-[tool=place]:[&_.react-flow\_\_pane]:cursor-crosshair!`,
|
|
51
|
+
rf`data-[tool=text]:[&_.react-flow\_\_pane]:cursor-text!`,
|
|
52
|
+
rf`data-[tool=connector]:[&_.react-flow\_\_node]:cursor-crosshair!`,
|
|
42
53
|
// keyboard focus on a card: a visible ring (the library turns it off)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
54
|
+
rf`[&_.react-flow\_\_node:focus-visible]:outline-2!`,
|
|
55
|
+
rf`[&_.react-flow\_\_node:focus-visible]:outline-offset-4!`,
|
|
56
|
+
rf`[&_.react-flow\_\_node:focus-visible]:outline-ring!`,
|
|
57
|
+
rf`[&_.react-flow\_\_node:focus-visible]:[outline-style:solid]!`,
|
|
47
58
|
// frames: only their title and edge take the pointer, so a double-click
|
|
48
59
|
// or a marquee inside one lands on the canvas (nodes.jsx)
|
|
49
|
-
|
|
60
|
+
rf`[&_.react-flow\_\_node-frame]:pointer-events-none!`,
|
|
50
61
|
// the resize handles on a selected card
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
rf`[&_.react-flow\_\_resize-control.handle]:size-2.5!`,
|
|
63
|
+
rf`[&_.react-flow\_\_resize-control.handle]:rounded-xs!`,
|
|
64
|
+
rf`[&_.react-flow\_\_resize-control.handle]:border-primary!`,
|
|
65
|
+
rf`[&_.react-flow\_\_resize-control.handle]:bg-background!`,
|
|
66
|
+
rf`[&_.react-flow\_\_resize-control.handle]:pointer-events-auto!`,
|
|
67
|
+
rf`[&_.react-flow\_\_resize-control.line]:border-primary/0!`,
|
|
68
|
+
rf`[&_.react-flow\_\_resize-control.line]:pointer-events-auto!`,
|
|
69
|
+
rf`[&_.react-flow\_\_resize-control.line.left]:w-2!`,
|
|
70
|
+
rf`[&_.react-flow\_\_resize-control.line.right]:w-2!`,
|
|
71
|
+
rf`[&_.react-flow\_\_resize-control.line.top]:h-2!`,
|
|
72
|
+
rf`[&_.react-flow\_\_resize-control.line.bottom]:h-2!`,
|
|
62
73
|
// the minimap: a card with the board in muted ink
|
|
63
74
|
"[--xy-minimap-background-color:var(--color-card)]!",
|
|
64
75
|
"[--xy-minimap-node-background-color:var(--color-muted-foreground)]!",
|
|
65
76
|
"[--xy-minimap-mask-background-color:var(--color-muted)]!",
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
77
|
+
rf`[&_.react-flow\_\_minimap]:rounded-lg!`,
|
|
78
|
+
rf`[&_.react-flow\_\_minimap]:border!`,
|
|
79
|
+
rf`[&_.react-flow\_\_minimap]:border-border!`,
|
|
80
|
+
rf`[&_.react-flow\_\_minimap]:shadow-md!`,
|
|
81
|
+
rf`[&_.react-flow\_\_minimap]:overflow-hidden!`,
|
|
82
|
+
rf`[&_.react-flow\_\_minimap-node]:opacity-50!`,
|
|
83
|
+
rf`[&_.react-flow\_\_minimap-mask]:opacity-70!`,
|
|
73
84
|
// React Flow's credit: kept (they ask for it), in the muted ink
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
rf`[&_.react-flow\_\_attribution]:bg-transparent!`,
|
|
86
|
+
rf`[&_.react-flow\_\_attribution_a]:text-muted-foreground!`,
|
|
76
87
|
// panels: ours sit in the corners with our own spacing
|
|
77
|
-
|
|
88
|
+
rf`[&_.react-flow\_\_panel]:m-3!`,
|
|
78
89
|
].join(" ");
|
|
79
90
|
|
|
80
91
|
// Colour TOKENS → classes, written out whole so Tailwind generates them. A
|
|
@@ -6,6 +6,13 @@ then "done" (closing itself a few seconds later) or the reason it
|
|
|
6
6
|
stopped. When no job runs it renders nothing — a panel that is always
|
|
7
7
|
there is a monitoring console, and apps do not have one.
|
|
8
8
|
|
|
9
|
+
The conversation is not here. Since 0.4.0 a job is a request, and its
|
|
10
|
+
conversation lives in the desk's agent column: the agent's questions,
|
|
11
|
+
the founder's answers and the yes before it acts are all there. The
|
|
12
|
+
panel says where the work is and points there — "Needs your answer"
|
|
13
|
+
with an Answer button when the agent waits on the founder, "Queued ·
|
|
14
|
+
after …" when it waits its turn.
|
|
15
|
+
|
|
9
16
|
```jsx
|
|
10
17
|
import { JobPanel } from "../components/blocks/job-panel"; // a Makersclaw app ("./blocks/job-panel" from components/)
|
|
11
18
|
import { JobPanel } from "@/components/makersclaw/blocks/job-panel"; // a shadcn project
|
|
@@ -26,11 +33,16 @@ pass those rows back in as `results`.
|
|
|
26
33
|
- `job` — what `useJob()` returns. Required. Outside Makersclaw, any
|
|
27
34
|
object of the same shape: `running`, `steps` (`[{ name, label, status:
|
|
28
35
|
"running" | "done" | "failed" }]`), `text`, `error`, `finishedAt`, and
|
|
29
|
-
`cancel()` / `reset()`.
|
|
36
|
+
`cancel()` / `reset()`. Optional, read when present (useJob sets them
|
|
37
|
+
since 0.4.0): `needsYou`, `queued`, `queuedAfter` (`{ after_title }`)
|
|
38
|
+
and `open()` — see "Needs you and queued" below. An object without
|
|
39
|
+
them (the 0.3.x shape) draws exactly as before.
|
|
30
40
|
- `title` — while it runs, what the agent is doing, in the founder's
|
|
31
41
|
words: `"Drafting the launch plan"`. Default `"Working on it"`.
|
|
32
42
|
- `doneTitle` — after a clean finish: `"Launch plan drafted"`.
|
|
33
43
|
- `failedTitle` — when it stopped with an error; the error follows it.
|
|
44
|
+
- `needsTitle` — while the agent waits on the founder's answer. Default
|
|
45
|
+
`"Needs your answer"`.
|
|
34
46
|
- `onRetry` — shows "Try again" beside Close when the job failed: start
|
|
35
47
|
the same job again.
|
|
36
48
|
- `autoClose` — milliseconds after a clean finish before the panel
|
|
@@ -40,12 +52,15 @@ pass those rows back in as `results`.
|
|
|
40
52
|
"N earlier steps" line.
|
|
41
53
|
- `showText` — the agent's own account under the steps, default true. It
|
|
42
54
|
is what the agent says it did, never the result: the result is rows.
|
|
55
|
+
It hides itself when `job.text` is empty, as it is on a request: the
|
|
56
|
+
account is in the desk's agent column.
|
|
43
57
|
- `className` — layout around it. Never colour.
|
|
44
58
|
|
|
45
59
|
### The generator (`variant="generator"`)
|
|
46
60
|
|
|
47
|
-
Everything above (`job`, `title`, `doneTitle`, `failedTitle`,
|
|
48
|
-
`autoClose`, `maxSteps`, `showText`) goes to the
|
|
61
|
+
Everything above (`job`, `title`, `doneTitle`, `failedTitle`,
|
|
62
|
+
`needsTitle`, `onRetry`, `autoClose`, `maxSteps`, `showText`) goes to the
|
|
63
|
+
panel inside it. Then:
|
|
49
64
|
|
|
50
65
|
- `onGenerate(prompt)` — start the job. Build the ask from the prompt and
|
|
51
66
|
your options and call `job.start(ask)`; return what it returns, or
|
|
@@ -72,6 +87,28 @@ Everything above (`job`, `title`, `doneTitle`, `failedTitle`, `onRetry`,
|
|
|
72
87
|
- `layout` — `"stacked"` (default: the form, then the shelf) or
|
|
73
88
|
`"split"` (side by side from lg up; stacked on a phone).
|
|
74
89
|
|
|
90
|
+
## Needs you and queued
|
|
91
|
+
|
|
92
|
+
A request can be live and still not moving. The panel draws two such
|
|
93
|
+
states from the job, and both count as running: no Close, Generate
|
|
94
|
+
stays off, and the panel stays until the job moves on.
|
|
95
|
+
|
|
96
|
+
- **Needs you** (`job.needsYou`) — the agent stopped to ask the founder
|
|
97
|
+
something, or for a yes before it acts. The heading is `needsTitle`
|
|
98
|
+
("Needs your answer"), the panel takes the warning tone, a line says
|
|
99
|
+
the question is in the agent column, and **Answer** takes Stop's
|
|
100
|
+
place: it calls `job.open()`, which opens the request's conversation
|
|
101
|
+
in the desk's agent column. The founder answers there (or stops it
|
|
102
|
+
there), never in the app; the job then runs on and the panel follows
|
|
103
|
+
it. A job with no `open()` has nowhere to point, so Stop stays.
|
|
104
|
+
- **Queued** (`job.queued`) — one job per list runs at a time, so this
|
|
105
|
+
one waits behind another. The heading names the job it waits for:
|
|
106
|
+
"Queued · after Find 20 leads" (`job.queuedAfter.after_title`, else
|
|
107
|
+
"the job before it"). Stop stays: it takes the job out of the queue.
|
|
108
|
+
|
|
109
|
+
When it runs again the heading is back to `title`, and the one step
|
|
110
|
+
under it is the request's line ("Drafting · 12 of 50").
|
|
111
|
+
|
|
75
112
|
## Example
|
|
76
113
|
|
|
77
114
|
```jsx
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "job-panel",
|
|
3
3
|
"title": "JobPanel",
|
|
4
|
-
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, then done or the reason it stopped — and nothing on screen when no job runs. variant=\"generator\" wraps it in a prompt, the surface's options, Generate and a results shelf, with no model, provider or API key of its own.",
|
|
4
|
+
"description": "The panel that follows work the founder asked the app's agent for (useJob): each step's label as it runs, Stop, \"Needs your answer\" with Answer while the agent waits on the founder, \"Queued · after …\" while it waits its turn, then done or the reason it stopped — and nothing on screen when no job runs. variant=\"generator\" wraps it in a prompt, the surface's options, Generate and a results shelf, with no model, provider or API key of its own.",
|
|
5
5
|
"useWhen": "A surface has a verb that asks the agent to make or find something (\"Generate a launch plan\", \"Find 20 leads\", \"Plan next sprint\") and the founder should watch it happen where the result will land. variant=\"generator\" when the screen IS the place to ask: a prompt and a few options, the results under them (product photos, ad copy, names).",
|
|
6
|
-
"tags": ["job", "agent", "generate", "progress", "steps", "running", "working", "commission", "ai", "stop", "status", "generator", "prompt", "results", "image", "studio"],
|
|
6
|
+
"tags": ["job", "agent", "generate", "progress", "steps", "running", "working", "commission", "ai", "stop", "status", "queued", "needs-you", "answer", "generator", "prompt", "results", "image", "studio"],
|
|
7
7
|
"exports": ["JobPanel"],
|
|
8
8
|
"files": ["index.js", "job-panel.jsx"],
|
|
9
9
|
"requires": []
|
|
@@ -2,7 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
// JobPanel driven by a pretend job — the gallery's and the smoke test's
|
|
4
4
|
// surface. Not installed into apps. Three variants behind the switcher:
|
|
5
|
-
// Panel the panel under a verb; one run succeeds, one fails
|
|
5
|
+
// Panel the panel under a verb; one run succeeds, one fails (both
|
|
6
|
+
// the 0.3.x shape), one waits its turn (queued), one stops
|
|
7
|
+
// to ask the founder (needsYou) — the last two a request's
|
|
8
|
+
// shape, the way useJob follows one since 0.4.0
|
|
6
9
|
// Photos variant="generator", stacked: a prompt, size and style, a
|
|
7
10
|
// grid of results
|
|
8
11
|
// Copy variant="generator", split: ad headlines as text results,
|
|
@@ -14,6 +17,24 @@ import { JobPanel } from "../components/blocks/job-panel";
|
|
|
14
17
|
const STEPS = ["Reading the product list", "Searching the web", "Drafting the plan", "Saving 6 tasks"];
|
|
15
18
|
const IDLE = { running: false, steps: [], text: "", error: null, finishedAt: null, progress: 0 };
|
|
16
19
|
|
|
20
|
+
const CLOSED = new Set(["done", "stopped", "didnt_finish"]);
|
|
21
|
+
|
|
22
|
+
// A request as useJob draws it since 0.4.0: one step, the request's line,
|
|
23
|
+
// and no text — the agent's account is in the desk's agent column.
|
|
24
|
+
function request(status, line, more = {}) {
|
|
25
|
+
return {
|
|
26
|
+
running: !CLOSED.has(status),
|
|
27
|
+
status,
|
|
28
|
+
queued: status === "queued",
|
|
29
|
+
queuedAfter: null,
|
|
30
|
+
needsYou: status === "needs_you",
|
|
31
|
+
liveLine: line,
|
|
32
|
+
steps: [{ name: "request", label: line, status: CLOSED.has(status) ? "done" : "running" }],
|
|
33
|
+
text: "",
|
|
34
|
+
...more,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
17
38
|
function usePretendJob() {
|
|
18
39
|
const [state, setState] = useState(IDLE);
|
|
19
40
|
const timers = useRef([]);
|
|
@@ -23,8 +44,34 @@ function usePretendJob() {
|
|
|
23
44
|
};
|
|
24
45
|
useEffect(() => clear, []);
|
|
25
46
|
const later = (ms, fn) => timers.current.push(setTimeout(fn, ms));
|
|
26
|
-
|
|
47
|
+
const moved = (next) => setState((s) => ({ ...s, ...next, progress: s.progress + 1 }));
|
|
48
|
+
|
|
49
|
+
// The work after the wait: counted progress, then a result.
|
|
50
|
+
function draft(from) {
|
|
51
|
+
for (let n = 0; n <= 6; n += 1) later(from + 450 * n, () => moved(request("working", `Drafting the plan · ${n} of 6`, { count: n, target: 6 })));
|
|
52
|
+
later(from + 450 * 6 + 500, () => moved({ ...request("done", "6 tasks saved to Launches"), finishedAt: new Date().toISOString() }));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// kind: "ok" | "fails" (the 0.3.x shape: steps and text) · "queued" |
|
|
56
|
+
// "asks" (a request's shape).
|
|
57
|
+
function start(kind = "ok") {
|
|
27
58
|
clear();
|
|
59
|
+
if (kind === "queued") {
|
|
60
|
+
setState({
|
|
61
|
+
...IDLE,
|
|
62
|
+
...request("queued", "Queued · after Find 20 leads", {
|
|
63
|
+
queuedAfter: { reason: "job", after_id: "req_leads", after_title: "Find 20 leads" },
|
|
64
|
+
}),
|
|
65
|
+
});
|
|
66
|
+
draft(2800);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (kind === "asks") {
|
|
70
|
+
setState({ ...IDLE, ...request("working", "Reading the product list") });
|
|
71
|
+
later(1200, () => moved(request("needs_you", "Needs you · Which launch should the plan cover?")));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const fail = kind === "fails";
|
|
28
75
|
setState({ ...IDLE, running: true });
|
|
29
76
|
STEPS.forEach((label, i) =>
|
|
30
77
|
later(700 * i + 200, () =>
|
|
@@ -53,12 +100,28 @@ function usePretendJob() {
|
|
|
53
100
|
start,
|
|
54
101
|
cancel: () => {
|
|
55
102
|
clear();
|
|
56
|
-
setState((s) => ({
|
|
103
|
+
setState((s) => ({
|
|
104
|
+
...s,
|
|
105
|
+
running: false,
|
|
106
|
+
queued: false,
|
|
107
|
+
needsYou: false,
|
|
108
|
+
status: s.status ? "stopped" : s.status,
|
|
109
|
+
finishedAt: new Date().toISOString(),
|
|
110
|
+
}));
|
|
57
111
|
},
|
|
58
112
|
reset: () => {
|
|
59
113
|
clear();
|
|
60
114
|
setState(IDLE);
|
|
61
115
|
},
|
|
116
|
+
// In an app this opens the question in the desk's agent column. The
|
|
117
|
+
// gallery has no desk, so the answer is pretended and the work goes on.
|
|
118
|
+
open: () => {
|
|
119
|
+
if (!state.needsYou) return false;
|
|
120
|
+
clear();
|
|
121
|
+
moved(request("working", "Drafting the plan"));
|
|
122
|
+
draft(900);
|
|
123
|
+
return true;
|
|
124
|
+
},
|
|
62
125
|
};
|
|
63
126
|
}
|
|
64
127
|
|
|
@@ -67,14 +130,20 @@ function PanelDemo() {
|
|
|
67
130
|
return (
|
|
68
131
|
<div className="grid max-w-xl gap-4">
|
|
69
132
|
<div className="flex flex-wrap gap-2">
|
|
70
|
-
<Button variant="secondary" onClick={() => job.start(
|
|
133
|
+
<Button variant="secondary" onClick={() => job.start("ok")} disabled={job.running}>
|
|
71
134
|
Generate a launch plan
|
|
72
135
|
</Button>
|
|
73
|
-
<Button variant="quiet" onClick={() => job.start(
|
|
136
|
+
<Button variant="quiet" onClick={() => job.start("fails")} disabled={job.running}>
|
|
74
137
|
Run one that fails
|
|
75
138
|
</Button>
|
|
139
|
+
<Button variant="quiet" onClick={() => job.start("queued")} disabled={job.running}>
|
|
140
|
+
Queue one behind another
|
|
141
|
+
</Button>
|
|
142
|
+
<Button variant="quiet" onClick={() => job.start("asks")} disabled={job.running}>
|
|
143
|
+
Run one that asks
|
|
144
|
+
</Button>
|
|
76
145
|
</div>
|
|
77
|
-
<JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" onRetry={() => job.start(
|
|
146
|
+
<JobPanel job={job} title="Drafting the launch plan" doneTitle="Launch plan drafted" onRetry={() => job.start("ok")} />
|
|
78
147
|
</div>
|
|
79
148
|
);
|
|
80
149
|
}
|