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
|
@@ -48,6 +48,7 @@ export { Stat, StatGroup, Delta, StatusDot, Legend, Meter } from "./stat";
|
|
|
48
48
|
export { Steps } from "./steps";
|
|
49
49
|
export { Separator, Kbd, KbdGroup, AspectRatio } from "./separator";
|
|
50
50
|
export { ScrollArea } from "./scroll-area";
|
|
51
|
+
export { AppLayout, DocumentLayout } from "./layout";
|
|
51
52
|
export { Slider } from "./slider";
|
|
52
53
|
export { InputGroup, PasswordInput } from "./input-group";
|
|
53
54
|
export { Image } from "./image";
|
|
@@ -17,7 +17,7 @@ import { Icon } from "./icon";
|
|
|
17
17
|
// meta a count, a date, a Badge — muted, on the right
|
|
18
18
|
// actions buttons on the far right (IconButton, Menu); they sit above
|
|
19
19
|
// the row's target, so they click on their own
|
|
20
|
-
// href a next/link row · `onClick` a button row · neither:
|
|
20
|
+
// href a next/link row · `onClick` a button row · neither: a plain row
|
|
21
21
|
// size default (56px) · sm (44px, for dense lists and rails) ·
|
|
22
22
|
// lg (64px, roomier: a pipeline's runs, a list read at leisure)
|
|
23
23
|
// variant plain (no fill) · outline (a bordered card) · muted (a filled panel)
|
|
@@ -58,7 +58,7 @@ const STACKED = {
|
|
|
58
58
|
|
|
59
59
|
const TARGET = "outline-none after:absolute after:inset-0";
|
|
60
60
|
|
|
61
|
-
// The row filled while its target (or a
|
|
61
|
+
// The row filled while its target (or a plain row itself) is current.
|
|
62
62
|
const CURRENT =
|
|
63
63
|
"has-[[data-item-target][aria-current]:not([aria-current=false])]:bg-accent " +
|
|
64
64
|
"[&[aria-current]:not([aria-current=false])]:bg-accent";
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
|
4
|
+
import { cx } from "./cx";
|
|
5
|
+
import { Button, IconButton } from "./button";
|
|
6
|
+
import { Sheet } from "./sheet";
|
|
7
|
+
|
|
8
|
+
// Layouts — who owns the window's height, and who scrolls.
|
|
9
|
+
//
|
|
10
|
+
// Every design-first build of 2026-10-07 shipped a shell of `min-h-dvh`
|
|
11
|
+
// that grew with its content: the page itself scrolled, no pane scrolled
|
|
12
|
+
// on its own, and a footer, an inspector or a button below a long
|
|
13
|
+
// article sat past the end of the iframe, where nothing could reach it.
|
|
14
|
+
// So the kit owns the window's height, once, and the app's code never
|
|
15
|
+
// sizes anything by the window (`npm run check` refuses min-h-dvh,
|
|
16
|
+
// h-screen, 100vh, h-[80vh]… outside the kit, rule 12).
|
|
17
|
+
//
|
|
18
|
+
// AppLayout — the app frame. Exactly one owns the viewport: the
|
|
19
|
+
// outermost. A layout inside another fills the slot it is in instead,
|
|
20
|
+
// whatever `height` says, so there is never a second viewport owner. The
|
|
21
|
+
// shell (app/shell.jsx) is the outermost one in every app the template
|
|
22
|
+
// lays; a surface that needs an inspector or a fill of its own returns
|
|
23
|
+
// an AppLayout as its outermost element, and it fills the shell's
|
|
24
|
+
// content edge to edge (the content's padding drops for it).
|
|
25
|
+
//
|
|
26
|
+
// header the top bar, always in view (title, the surface's
|
|
27
|
+
// commands). The bar draws its own bottom border
|
|
28
|
+
// navigation the surfaces, a page tree: a rail beside the content
|
|
29
|
+
// from md (768px); below md a Sheet from the leading
|
|
30
|
+
// edge, opened by the menu button in the top bar. A
|
|
31
|
+
// press on a link or a button inside closes it
|
|
32
|
+
// navigationLabel the Sheet's title and the menu button's words
|
|
33
|
+
// ("Navigation")
|
|
34
|
+
// navigationWidth the rail, in px (176; 160 to 320)
|
|
35
|
+
// children the content. It scrolls inside its own region
|
|
36
|
+
// contentMode scroll (the default): the content region scrolls ·
|
|
37
|
+
// fill: the region is a bounded flex column and the
|
|
38
|
+
// child owns its own scrolling (a canvas, a grid, a
|
|
39
|
+
// spreadsheet, an editor that scrolls itself)
|
|
40
|
+
// contentLabel the content region's name for a screen reader ("Content")
|
|
41
|
+
// contentClassName spacing and token backgrounds for the content
|
|
42
|
+
// region (p-4 sm:p-6, bg-card). Never a height or an
|
|
43
|
+
// overflow: the layout owns those
|
|
44
|
+
// scrollKey when it changes the content scrolls back to its top
|
|
45
|
+
// and the navigation Sheet closes: pass the route
|
|
46
|
+
// (usePathname()), since the frame stays mounted while
|
|
47
|
+
// the surfaces change inside it
|
|
48
|
+
// inspector the details, review or feedback panel beside the
|
|
49
|
+
// content, scrolling on its own
|
|
50
|
+
// inspectorLabel the inspector region's name ("Details")
|
|
51
|
+
// inspectorWidth in px (360; 240 to 560). Builds have needed 300 and 440
|
|
52
|
+
// inspectorAt md · lg (the default) · xl: the width from which the
|
|
53
|
+
// inspector sits beside the content
|
|
54
|
+
// footer under the content, always in view (the save state,
|
|
55
|
+
// the composer, the commands for what is above)
|
|
56
|
+
// height viewport (the default): the frame is the window's
|
|
57
|
+
// height · parent: it fills a bounded parent. Nested,
|
|
58
|
+
// it is parent whatever this says
|
|
59
|
+
// className the frame's own tokens; never a height or an overflow
|
|
60
|
+
//
|
|
61
|
+
// Below `inspectorAt`, where there is no room for two columns:
|
|
62
|
+
// - contentMode scroll: the content and the inspector share ONE scroll
|
|
63
|
+
// region, the inspector after the content. Everything is reachable
|
|
64
|
+
// by scrolling, and nothing is hidden behind a button.
|
|
65
|
+
// - contentMode fill: the content keeps the room and the inspector is
|
|
66
|
+
// a bounded pane under it (at most 45% of the height), scrolling on
|
|
67
|
+
// its own. A fill child has no flow to stack into, and a pane with
|
|
68
|
+
// no bound under it is cut off (workspace-layout's fault, 2026-10-07).
|
|
69
|
+
// The footer stays under both, in view.
|
|
70
|
+
//
|
|
71
|
+
// In print the frame lets go: no bound, no overflow, no navigation, no
|
|
72
|
+
// header, inspector or footer; the content flows onto pages. A page meant
|
|
73
|
+
// for print or export is a DocumentLayout inside the content.
|
|
74
|
+
|
|
75
|
+
const Nested = createContext(false);
|
|
76
|
+
|
|
77
|
+
const px = (value, fallback, min, max) => {
|
|
78
|
+
const n = Number(value);
|
|
79
|
+
return `${Number.isFinite(n) && n > 0 ? Math.min(max, Math.max(min, Math.round(n))) : fallback}px`;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
// Tailwind reads class names whole from the source, so every breakpoint
|
|
83
|
+
// is written out.
|
|
84
|
+
const BESIDE = {
|
|
85
|
+
md: {
|
|
86
|
+
main: "md:grid md:grid-cols-[minmax(0,1fr)_var(--app-inspector)] md:grid-rows-[minmax(0,1fr)_auto]",
|
|
87
|
+
flow: "md:contents",
|
|
88
|
+
content: "md:col-start-1 md:row-start-1",
|
|
89
|
+
scrolls: "md:min-h-0 md:overflow-y-auto md:overscroll-contain",
|
|
90
|
+
inspector: "md:col-start-2 md:row-span-2 md:row-start-1 md:max-h-none md:min-h-0 md:overflow-y-auto md:border-l md:border-t-0",
|
|
91
|
+
footer: "md:col-start-1 md:row-start-2",
|
|
92
|
+
},
|
|
93
|
+
lg: {
|
|
94
|
+
main: "lg:grid lg:grid-cols-[minmax(0,1fr)_var(--app-inspector)] lg:grid-rows-[minmax(0,1fr)_auto]",
|
|
95
|
+
flow: "lg:contents",
|
|
96
|
+
content: "lg:col-start-1 lg:row-start-1",
|
|
97
|
+
scrolls: "lg:min-h-0 lg:overflow-y-auto lg:overscroll-contain",
|
|
98
|
+
inspector: "lg:col-start-2 lg:row-span-2 lg:row-start-1 lg:max-h-none lg:min-h-0 lg:overflow-y-auto lg:border-l lg:border-t-0",
|
|
99
|
+
footer: "lg:col-start-1 lg:row-start-2",
|
|
100
|
+
},
|
|
101
|
+
xl: {
|
|
102
|
+
main: "xl:grid xl:grid-cols-[minmax(0,1fr)_var(--app-inspector)] xl:grid-rows-[minmax(0,1fr)_auto]",
|
|
103
|
+
flow: "xl:contents",
|
|
104
|
+
content: "xl:col-start-1 xl:row-start-1",
|
|
105
|
+
scrolls: "xl:min-h-0 xl:overflow-y-auto xl:overscroll-contain",
|
|
106
|
+
inspector: "xl:col-start-2 xl:row-span-2 xl:row-start-1 xl:max-h-none xl:min-h-0 xl:overflow-y-auto xl:border-l xl:border-t-0",
|
|
107
|
+
footer: "xl:col-start-1 xl:row-start-2",
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const REGION = "outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring";
|
|
112
|
+
const LETS_GO = "print:block print:h-auto print:min-h-0 print:overflow-visible";
|
|
113
|
+
|
|
114
|
+
export function AppLayout({
|
|
115
|
+
header,
|
|
116
|
+
navigation,
|
|
117
|
+
navigationLabel = "Navigation",
|
|
118
|
+
navigationWidth,
|
|
119
|
+
children,
|
|
120
|
+
contentMode = "scroll",
|
|
121
|
+
contentLabel = "Content",
|
|
122
|
+
contentClassName,
|
|
123
|
+
scrollKey,
|
|
124
|
+
inspector,
|
|
125
|
+
inspectorLabel = "Details",
|
|
126
|
+
inspectorWidth,
|
|
127
|
+
inspectorAt = "lg",
|
|
128
|
+
footer,
|
|
129
|
+
height = "viewport",
|
|
130
|
+
className,
|
|
131
|
+
}) {
|
|
132
|
+
const nested = useContext(Nested);
|
|
133
|
+
const bounded = nested || height === "parent";
|
|
134
|
+
const fill = contentMode === "fill";
|
|
135
|
+
const at = BESIDE[inspectorAt] ?? BESIDE.lg;
|
|
136
|
+
const hasInspector = inspector != null && inspector !== false;
|
|
137
|
+
const hasNavigation = navigation != null && navigation !== false;
|
|
138
|
+
const shared = hasInspector && !fill; // below `at`: content and inspector in one scroll region
|
|
139
|
+
const [open, setOpen] = useState(false);
|
|
140
|
+
const flowRef = useRef(null);
|
|
141
|
+
const contentRef = useRef(null);
|
|
142
|
+
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
if (flowRef.current) flowRef.current.scrollTop = 0;
|
|
145
|
+
if (contentRef.current) contentRef.current.scrollTop = 0;
|
|
146
|
+
setOpen(false);
|
|
147
|
+
}, [scrollKey]);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<Nested.Provider value={true}>
|
|
151
|
+
<div
|
|
152
|
+
data-app-layout={bounded ? "parent" : "viewport"}
|
|
153
|
+
style={{ "--app-nav": px(navigationWidth, 176, 160, 320), "--app-inspector": px(inspectorWidth, 360, 240, 560) }}
|
|
154
|
+
className={cx(
|
|
155
|
+
"flex min-w-0 flex-col overflow-hidden bg-background text-foreground",
|
|
156
|
+
bounded ? "h-full min-h-0 flex-1" : "h-dvh",
|
|
157
|
+
LETS_GO,
|
|
158
|
+
className,
|
|
159
|
+
)}
|
|
160
|
+
>
|
|
161
|
+
{header != null && header !== false ? (
|
|
162
|
+
<div data-layout-header className="flex shrink-0 items-center gap-1 border-b border-border print:hidden">
|
|
163
|
+
{hasNavigation ? (
|
|
164
|
+
<IconButton icon="menu" label={`Open ${navigationLabel.toLowerCase()}`} onClick={() => setOpen(true)} className="ml-1 md:hidden" />
|
|
165
|
+
) : null}
|
|
166
|
+
<div className="min-w-0 flex-1">{header}</div>
|
|
167
|
+
</div>
|
|
168
|
+
) : hasNavigation ? (
|
|
169
|
+
<div data-layout-header className="flex shrink-0 items-center border-b border-border px-1 py-1 md:hidden print:hidden">
|
|
170
|
+
<Button variant="quiet" size="sm" icon="menu" onClick={() => setOpen(true)}>
|
|
171
|
+
{navigationLabel}
|
|
172
|
+
</Button>
|
|
173
|
+
</div>
|
|
174
|
+
) : null}
|
|
175
|
+
|
|
176
|
+
{hasNavigation ? (
|
|
177
|
+
<Sheet open={open} onClose={() => setOpen(false)} title={navigationLabel} side="start">
|
|
178
|
+
{open ? (
|
|
179
|
+
<div
|
|
180
|
+
onClick={(e) => {
|
|
181
|
+
if (e.target instanceof Element && e.target.closest("a,button")) setOpen(false);
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{navigation}
|
|
185
|
+
</div>
|
|
186
|
+
) : null}
|
|
187
|
+
</Sheet>
|
|
188
|
+
) : null}
|
|
189
|
+
|
|
190
|
+
<div data-layout-body className={cx("flex min-h-0 min-w-0 flex-1", LETS_GO)}>
|
|
191
|
+
{hasNavigation ? (
|
|
192
|
+
<div
|
|
193
|
+
data-layout-navigation
|
|
194
|
+
className="hidden shrink-0 flex-col overflow-y-auto overscroll-contain border-r border-border md:flex md:w-(--app-nav) print:hidden"
|
|
195
|
+
>
|
|
196
|
+
{navigation}
|
|
197
|
+
</div>
|
|
198
|
+
) : null}
|
|
199
|
+
|
|
200
|
+
<div data-layout-main className={cx("flex min-h-0 min-w-0 flex-1 flex-col", hasInspector && at.main, LETS_GO)}>
|
|
201
|
+
<div
|
|
202
|
+
ref={flowRef}
|
|
203
|
+
data-layout-flow
|
|
204
|
+
className={cx(
|
|
205
|
+
"flex min-h-0 min-w-0 flex-1 flex-col",
|
|
206
|
+
shared && "overflow-y-auto overscroll-contain",
|
|
207
|
+
hasInspector && at.flow,
|
|
208
|
+
LETS_GO,
|
|
209
|
+
)}
|
|
210
|
+
>
|
|
211
|
+
<div
|
|
212
|
+
ref={contentRef}
|
|
213
|
+
data-layout-content
|
|
214
|
+
role="region"
|
|
215
|
+
aria-label={contentLabel}
|
|
216
|
+
tabIndex={0}
|
|
217
|
+
className={cx(
|
|
218
|
+
"flex min-w-0 flex-col",
|
|
219
|
+
REGION,
|
|
220
|
+
fill
|
|
221
|
+
? "min-h-0 flex-1 overflow-hidden"
|
|
222
|
+
: shared
|
|
223
|
+
? cx("shrink-0", at.scrolls)
|
|
224
|
+
: "min-h-0 flex-1 overflow-y-auto overscroll-contain",
|
|
225
|
+
hasInspector && at.content,
|
|
226
|
+
LETS_GO,
|
|
227
|
+
contentClassName,
|
|
228
|
+
// A surface that is a frame of its own runs to the edges.
|
|
229
|
+
"has-[>[data-app-layout]]:p-0",
|
|
230
|
+
)}
|
|
231
|
+
>
|
|
232
|
+
{children}
|
|
233
|
+
</div>
|
|
234
|
+
{hasInspector ? (
|
|
235
|
+
<div
|
|
236
|
+
data-layout-inspector
|
|
237
|
+
role="region"
|
|
238
|
+
aria-label={inspectorLabel}
|
|
239
|
+
tabIndex={0}
|
|
240
|
+
className={cx(
|
|
241
|
+
"min-w-0 shrink-0 border-t border-border print:hidden",
|
|
242
|
+
REGION,
|
|
243
|
+
fill && "max-h-[45%] overflow-y-auto overscroll-contain",
|
|
244
|
+
at.inspector,
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
{inspector}
|
|
248
|
+
</div>
|
|
249
|
+
) : null}
|
|
250
|
+
</div>
|
|
251
|
+
{footer != null && footer !== false ? (
|
|
252
|
+
<div data-layout-footer className={cx("shrink-0 border-t border-border print:hidden", hasInspector && at.footer)}>
|
|
253
|
+
{footer}
|
|
254
|
+
</div>
|
|
255
|
+
) : null}
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
</Nested.Provider>
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// DocumentLayout — a page that flows: a report, an invoice, an article
|
|
264
|
+
// meant for print or export. It owns no height and scrolls nothing; it
|
|
265
|
+
// sits in the AppLayout's content (contentMode scroll), which scrolls it,
|
|
266
|
+
// and in print the frame lets go and it flows onto pages.
|
|
267
|
+
//
|
|
268
|
+
// children the document
|
|
269
|
+
// toolbar a bar held at the top while the document scrolls under it
|
|
270
|
+
// (Print, Export, the zoom); never printed
|
|
271
|
+
// width prose (the default: a measure for reading) · page (A4,
|
|
272
|
+
// 210mm) · letter (8.5in) · full. In print it is the paper's
|
|
273
|
+
// paper true: drawn as a sheet on the desk on screen (bg-card, a
|
|
274
|
+
// hairline, padding); plain in print
|
|
275
|
+
// align start (the default) · center
|
|
276
|
+
// label the document's name for a screen reader ("Document")
|
|
277
|
+
// className the document's own spacing and tokens; never a height
|
|
278
|
+
const WIDTHS = {
|
|
279
|
+
prose: "max-w-prose",
|
|
280
|
+
page: "max-w-[210mm]",
|
|
281
|
+
letter: "max-w-[8.5in]",
|
|
282
|
+
full: "max-w-none",
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
export function DocumentLayout({ children, toolbar, width = "prose", paper = false, align = "start", label = "Document", className }) {
|
|
286
|
+
return (
|
|
287
|
+
<div data-document-layout className="flex min-w-0 flex-col">
|
|
288
|
+
{toolbar != null && toolbar !== false ? (
|
|
289
|
+
<div data-document-toolbar className="sticky top-0 z-10 border-b border-border bg-background print:hidden">
|
|
290
|
+
{toolbar}
|
|
291
|
+
</div>
|
|
292
|
+
) : null}
|
|
293
|
+
<article
|
|
294
|
+
aria-label={label}
|
|
295
|
+
className={cx(
|
|
296
|
+
"w-full min-w-0 print:max-w-none",
|
|
297
|
+
WIDTHS[width] ?? WIDTHS.prose,
|
|
298
|
+
align === "center" && "mx-auto",
|
|
299
|
+
paper && "my-6 rounded-lg border border-border bg-card p-8 text-card-foreground shadow-sm print:my-0 print:rounded-none print:border-0 print:p-0 print:shadow-none",
|
|
300
|
+
className,
|
|
301
|
+
)}
|
|
302
|
+
>
|
|
303
|
+
{children}
|
|
304
|
+
</article>
|
|
305
|
+
</div>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
@@ -2,7 +2,7 @@ import { cx } from "./cx";
|
|
|
2
2
|
import { IconButton } from "./button";
|
|
3
3
|
import { Icon } from "./icon";
|
|
4
4
|
|
|
5
|
-
//
|
|
5
|
+
// Plain parts: a card, a notice, a badge, a kicker, the empty state.
|
|
6
6
|
// Server-safe (no hooks), so a server surface can use them directly.
|
|
7
7
|
|
|
8
8
|
const TONES = {
|
|
@@ -2,13 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
import { headers } from "next/headers";
|
|
4
4
|
import { revalidatePath } from "next/cache";
|
|
5
|
-
import { ident, logActivity, query } from "./db";
|
|
5
|
+
import { ident, logActivity, query, requestMember, transaction } from "./db";
|
|
6
6
|
|
|
7
7
|
// Every write the app makes. Callable from server and client components
|
|
8
8
|
// alike (they are server actions). Each one goes through lib/db.js,
|
|
9
9
|
// records an activity row, and re-renders the app. Table and column names
|
|
10
10
|
// are quoted through ident(), which refuses anything that is not a plain
|
|
11
|
-
// identifier. Returns { ok } or { ok: false, error }; never throws.
|
|
11
|
+
// identifier. Returns { ok } or { ok: false, error, code }; never throws.
|
|
12
|
+
// `code` is the one the platform's data door answers a browser app with
|
|
13
|
+
// (lib/door.js), so a surface branches on it the same way in both modes.
|
|
14
|
+
//
|
|
15
|
+
// Each write runs in its own transaction that first tells the database
|
|
16
|
+
// who is asking (transaction() in lib/db.js: claw.member_id and
|
|
17
|
+
// claw.member_email, local to it), so a trigger or an action function
|
|
18
|
+
// reads the member here as it does in a browser app, behind the platform's
|
|
19
|
+
// door. The activity line is written after the COMMIT, never inside it:
|
|
20
|
+
// a line that fails must not undo the write it describes.
|
|
12
21
|
//
|
|
13
22
|
// `surface` is optional: proxy.js stamps the current surface key on
|
|
14
23
|
// the request, so a surface rarely needs to pass it.
|
|
@@ -20,13 +29,12 @@ import { ident, logActivity, query } from "./db";
|
|
|
20
29
|
// words. Pass a detail when the surface can say what changed in plain
|
|
21
30
|
// words; leave it out and the tab shows the action word alone.
|
|
22
31
|
|
|
32
|
+
/** The line's `by`, as the door writes it: the member's email, else
|
|
33
|
+
* their id, else unknown (a local run). */
|
|
23
34
|
async function actor() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
} catch {
|
|
28
|
-
return "user:unknown";
|
|
29
|
-
}
|
|
35
|
+
const { id, email } = await requestMember();
|
|
36
|
+
if (email) return `user:${email}`;
|
|
37
|
+
return id ? `user:${id}` : "user:unknown";
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
async function surfaceOf(explicit) {
|
|
@@ -97,36 +105,88 @@ async function setClause(source, cols) {
|
|
|
97
105
|
return cols.map((c, i) => `${ident(c)} = $${i + 2}`).join(", ") + (await stampFor(source, cols));
|
|
98
106
|
}
|
|
99
107
|
|
|
100
|
-
|
|
108
|
+
// Postgres's raise_exception: what a plpgsql `raise exception` says.
|
|
109
|
+
const REFUSED = "P0001";
|
|
110
|
+
|
|
111
|
+
// The data door's codes for Postgres's errors (claw's STATIC-SPEC §1,
|
|
112
|
+
// "Errors reach the Builder"). Up to 0.6.x a failure here had no code, so
|
|
113
|
+
// a surface that branched on the door's `code` in a browser app fell
|
|
114
|
+
// through to its generic path after `makersclaw mode server`.
|
|
115
|
+
const CODES = new Map([
|
|
116
|
+
[REFUSED, "action_refused"],
|
|
117
|
+
["23505", "conflict"], // unique
|
|
118
|
+
["23503", "conflict"], // foreign key
|
|
119
|
+
["23502", "bad_request"], // not null
|
|
120
|
+
["22P02", "bad_request"], // not a value of the column's type
|
|
121
|
+
["22007", "bad_request"], // not a date or time
|
|
122
|
+
["22008", "bad_request"], // a date or time out of range
|
|
123
|
+
["57014", "too_slow"],
|
|
124
|
+
["40001", "try_again"], // serialization
|
|
125
|
+
["40P01", "try_again"], // deadlock
|
|
126
|
+
["55P03", "try_again"], // lock not available
|
|
127
|
+
]);
|
|
128
|
+
const SQLSTATE = /^[0-9A-Z]{5}$/;
|
|
129
|
+
|
|
130
|
+
/** A failure's code, as the door gives it. `missing` names the one error
|
|
131
|
+
* that means the statement's own table or function is not there: 42P01
|
|
132
|
+
* no_such_table for a table's write, 42883 no_such_action for an
|
|
133
|
+
* action. Postgres raises it with no `where` (the context an error from
|
|
134
|
+
* inside a function or a trigger carries); from inside, it is the
|
|
135
|
+
* code's fault, bad_request, as the rest of class 42. ident()'s refusal
|
|
136
|
+
* is bad_request; a connection that failed is failed. */
|
|
137
|
+
function codeOf(error, missing) {
|
|
138
|
+
const state = typeof error?.code === "string" && SQLSTATE.test(error.code) ? error.code : "";
|
|
139
|
+
if (!state) return /^invalid identifier: /.test(error?.message ?? "") ? "bad_request" : "failed";
|
|
140
|
+
if (missing?.[state] && !error.where) return missing[state];
|
|
141
|
+
if (CODES.has(state)) return CODES.get(state);
|
|
142
|
+
return state.startsWith("42") ? "bad_request" : "failed";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function fail(error, missing) {
|
|
101
146
|
const message = error?.message ?? String(error);
|
|
102
|
-
|
|
103
|
-
|
|
147
|
+
const code = codeOf(error, missing);
|
|
148
|
+
// A trigger's own raise exception is a deliberate no, as an action's is
|
|
149
|
+
// (runAction): a warning, or the log tail sends the platform agent after it.
|
|
150
|
+
if (code === "action_refused") console.warn(`[actions] refused: ${message}`);
|
|
151
|
+
else console.error(`[actions] write failed: ${message}`);
|
|
152
|
+
return { ok: false, error: message, code };
|
|
104
153
|
}
|
|
105
154
|
|
|
155
|
+
/** A write refused here, with the door's code for it. */
|
|
156
|
+
const refused = (error, code) => ({ ok: false, error, code });
|
|
157
|
+
|
|
158
|
+
/** What the door reads a table's write failing with, when the table is not there. */
|
|
159
|
+
const NO_TABLE = { "42P01": "no_such_table" };
|
|
160
|
+
|
|
106
161
|
/** Change columns of one row. `patch` is { column: value }; updated_at
|
|
107
|
-
* becomes now() when the table has one (above).
|
|
108
|
-
|
|
162
|
+
* becomes now() when the table has one (above). `log: false` writes no
|
|
163
|
+
* activity line: for a save that is typing, not a decision (an autosave
|
|
164
|
+
* every few keystrokes would flood the Activity tab). */
|
|
165
|
+
export async function updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log }) {
|
|
109
166
|
try {
|
|
167
|
+
const table = ident(source);
|
|
168
|
+
const key = ident(idColumn);
|
|
110
169
|
const cols = Object.keys(patch ?? {}).filter((c) => patch[c] !== undefined);
|
|
111
|
-
if (!cols.length) return
|
|
170
|
+
if (!cols.length) return refused("nothing to change", "bad_request");
|
|
112
171
|
const sets = await setClause(source, cols);
|
|
113
|
-
const { rowCount } = await
|
|
114
|
-
`UPDATE ${
|
|
115
|
-
[id, ...cols.map((c) => patch[c])],
|
|
172
|
+
const { rowCount } = await transaction((q) =>
|
|
173
|
+
q(`UPDATE ${table} SET ${sets} WHERE ${key} = $1`, [id, ...cols.map((c) => patch[c])]),
|
|
116
174
|
);
|
|
117
|
-
if (!rowCount) return
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
175
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
176
|
+
if (log !== false) {
|
|
177
|
+
await logActivity({
|
|
178
|
+
action,
|
|
179
|
+
surface: await surfaceOf(surface),
|
|
180
|
+
row_ref: id,
|
|
181
|
+
detail: detail ?? `${cols.join(", ")} changed`,
|
|
182
|
+
data: data ?? patch,
|
|
183
|
+
by: await actor(),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
126
186
|
done();
|
|
127
187
|
return { ok: true };
|
|
128
188
|
} catch (error) {
|
|
129
|
-
return fail(error);
|
|
189
|
+
return fail(error, NO_TABLE);
|
|
130
190
|
}
|
|
131
191
|
}
|
|
132
192
|
|
|
@@ -148,12 +208,10 @@ export async function moveRow({ source, idColumn = "id", id, stateColumn, to, su
|
|
|
148
208
|
export async function insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) {
|
|
149
209
|
try {
|
|
150
210
|
const cols = Object.keys(values ?? {}).filter((c) => values[c] !== undefined);
|
|
151
|
-
if (!cols.length) return
|
|
211
|
+
if (!cols.length) return refused("nothing to add", "bad_request");
|
|
152
212
|
const placeholders = cols.map((_, i) => `$${i + 1}`).join(", ");
|
|
153
|
-
const {
|
|
154
|
-
|
|
155
|
-
cols.map((c) => values[c]),
|
|
156
|
-
);
|
|
213
|
+
const text = `INSERT INTO ${ident(source)} (${cols.map(ident).join(", ")}) VALUES (${placeholders}) RETURNING ${ident(idColumn)}`;
|
|
214
|
+
const { rows } = await transaction((q) => q(text, cols.map((c) => values[c])));
|
|
157
215
|
const id = rows[0]?.[idColumn];
|
|
158
216
|
await logActivity({
|
|
159
217
|
action,
|
|
@@ -166,14 +224,15 @@ export async function insertRow({ source, idColumn = "id", values, surface, acti
|
|
|
166
224
|
done();
|
|
167
225
|
return { ok: true, id };
|
|
168
226
|
} catch (error) {
|
|
169
|
-
return fail(error);
|
|
227
|
+
return fail(error, NO_TABLE);
|
|
170
228
|
}
|
|
171
229
|
}
|
|
172
230
|
|
|
173
231
|
export async function deleteRow({ source, idColumn = "id", id, surface, detail }) {
|
|
174
232
|
try {
|
|
175
|
-
const
|
|
176
|
-
|
|
233
|
+
const text = `DELETE FROM ${ident(source)} WHERE ${ident(idColumn)} = $1`;
|
|
234
|
+
const { rowCount } = await transaction((q) => q(text, [id]));
|
|
235
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
177
236
|
await logActivity({
|
|
178
237
|
action: "removed",
|
|
179
238
|
surface: await surfaceOf(surface),
|
|
@@ -184,7 +243,7 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
|
|
|
184
243
|
done();
|
|
185
244
|
return { ok: true };
|
|
186
245
|
} catch (error) {
|
|
187
|
-
return fail(error);
|
|
246
|
+
return fail(error, NO_TABLE);
|
|
188
247
|
}
|
|
189
248
|
}
|
|
190
249
|
|
|
@@ -195,7 +254,8 @@ export async function deleteRow({ source, idColumn = "id", id, surface, detail }
|
|
|
195
254
|
* has a plain `key text` column and Postgres rejects ON CONFLICT without a
|
|
196
255
|
* unique index on it. JSON mode sets the key inside the jsonb `column` of
|
|
197
256
|
* the row `id` in `source`. An update stamps updated_at as updateRow's
|
|
198
|
-
* does; the insert leaves it to the column's default.
|
|
257
|
+
* does; the insert leaves it to the column's default. The UPDATE and the
|
|
258
|
+
* INSERT run in one transaction (up to 0.6.x they were two).
|
|
199
259
|
*/
|
|
200
260
|
export async function saveSetting({
|
|
201
261
|
mode = "table",
|
|
@@ -210,24 +270,21 @@ export async function saveSetting({
|
|
|
210
270
|
surface,
|
|
211
271
|
}) {
|
|
212
272
|
try {
|
|
213
|
-
if (!key) return
|
|
273
|
+
if (!key) return refused("no key", "bad_request");
|
|
214
274
|
if (mode === "json") {
|
|
275
|
+
const col = ident(column);
|
|
215
276
|
const stamp = await stampFor(source, [column]);
|
|
216
|
-
const {
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
);
|
|
220
|
-
if (!rowCount) return { ok: false, error: "row not found" };
|
|
277
|
+
const text = `UPDATE ${ident(source)} SET ${col} = jsonb_set(COALESCE(${col}, '{}'::jsonb), $2::text[], $3::jsonb, true)${stamp} WHERE ${ident(idColumn)} = $1`;
|
|
278
|
+
const { rowCount } = await transaction((q) => q(text, [id, [key], JSON.stringify(value ?? null)]));
|
|
279
|
+
if (!rowCount) return refused("row not found", "not_found");
|
|
221
280
|
} else {
|
|
222
281
|
const stored = typeof value === "object" && value !== null ? JSON.stringify(value) : value;
|
|
282
|
+
const [table, keyCol, valueCol] = [ident(source), ident(keyColumn), ident(valueColumn)];
|
|
223
283
|
const stamp = await stampFor(source, [valueColumn]);
|
|
224
|
-
|
|
225
|
-
`UPDATE ${
|
|
226
|
-
[key, stored]
|
|
227
|
-
);
|
|
228
|
-
if (!rowCount) {
|
|
229
|
-
await query(`INSERT INTO ${ident(source)} (${ident(keyColumn)}, ${ident(valueColumn)}) VALUES ($1, $2)`, [key, stored]);
|
|
230
|
-
}
|
|
284
|
+
await transaction(async (q) => {
|
|
285
|
+
const { rowCount } = await q(`UPDATE ${table} SET ${valueCol} = $2${stamp} WHERE ${keyCol} = $1`, [key, stored]);
|
|
286
|
+
if (!rowCount) await q(`INSERT INTO ${table} (${keyCol}, ${valueCol}) VALUES ($1, $2)`, [key, stored]);
|
|
287
|
+
});
|
|
231
288
|
}
|
|
232
289
|
await logActivity({
|
|
233
290
|
action: "edited",
|
|
@@ -240,6 +297,68 @@ export async function saveSetting({
|
|
|
240
297
|
done();
|
|
241
298
|
return { ok: true };
|
|
242
299
|
} catch (error) {
|
|
243
|
-
return fail(error);
|
|
300
|
+
return fail(error, NO_TABLE);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// An action's name as the door takes it: one bare, lower-case name. The
|
|
305
|
+
// door calls only a function of the app's own schema, so a name with a
|
|
306
|
+
// schema (`app_x.fn`) or capitals (`CloseCycle`), which ident() quotes
|
|
307
|
+
// and Postgres would find, worked here and is no_such_action there.
|
|
308
|
+
// Refused here too, so a surface that works in one mode works in the
|
|
309
|
+
// other.
|
|
310
|
+
const ACTION_NAME = /^[a-z_][a-z0-9_]{0,62}$/;
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Run an action: a function in the app's own schema, `(args jsonb)
|
|
314
|
+
* returns jsonb`, for any write of more than one row, or one that decides
|
|
315
|
+
* something. The screen calls it here; the app's agent runs the same
|
|
316
|
+
* function with `sql` (`select close_cycle($1::jsonb)`), so the two never
|
|
317
|
+
* drift. It runs inside transaction(), after the member is set, so it
|
|
318
|
+
* reads who is asking from `current_setting('claw.member_email', true)`.
|
|
319
|
+
* It says no with `raise exception '<sentence>'`: the sentence comes back
|
|
320
|
+
* as `error`, code action_refused, and the log says it was refused, not
|
|
321
|
+
* that the app failed. A name that is not one bare lower-case name, or no
|
|
322
|
+
* function of it that takes jsonb, is no_such_action, as at the door.
|
|
323
|
+
*
|
|
324
|
+
* The activity line is this call's: `action` defaults to the function's
|
|
325
|
+
* name, `row_ref` is `rowRef` or the result's `id`. The function never
|
|
326
|
+
* writes claw_app.activity itself. `log: false` writes no line (an
|
|
327
|
+
* autosave). `args` defaults to {}.
|
|
328
|
+
* → { ok: true, result } | { ok: false, error, code }
|
|
329
|
+
*/
|
|
330
|
+
export async function runAction({ name, args, surface, action, detail, rowRef, log } = {}) {
|
|
331
|
+
if (typeof name !== "string" || !ACTION_NAME.test(name)) {
|
|
332
|
+
const error = `no action ${JSON.stringify(name ?? null)}: an action is named in lower case, a-z, 0-9 and _, with no schema (close_cycle)`;
|
|
333
|
+
console.error(`[actions] ${error}`);
|
|
334
|
+
return refused(error, "no_such_action");
|
|
335
|
+
}
|
|
336
|
+
try {
|
|
337
|
+
const text = `SELECT ${ident(name)}($1::jsonb) AS result`;
|
|
338
|
+
const json = JSON.stringify(args === undefined ? {} : args);
|
|
339
|
+
const { rows } = await transaction((q) => q(text, [json]));
|
|
340
|
+
const result = rows[0]?.result ?? null;
|
|
341
|
+
if (log !== false) {
|
|
342
|
+
const returned = result && typeof result === "object" && !Array.isArray(result) ? result.id : undefined;
|
|
343
|
+
await logActivity({
|
|
344
|
+
action: action || name,
|
|
345
|
+
surface: await surfaceOf(surface),
|
|
346
|
+
row_ref: rowRef ?? returned ?? null,
|
|
347
|
+
detail: detail ?? null,
|
|
348
|
+
by: await actor(),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
done();
|
|
352
|
+
return { ok: true, result };
|
|
353
|
+
} catch (error) {
|
|
354
|
+
// P0001 is the function's own raise exception: a deliberate no ("that
|
|
355
|
+
// cycle is already closed"), which the member acts on. Logged as an
|
|
356
|
+
// error, the log tail would send the platform agent chasing it as a
|
|
357
|
+
// bug, which the door avoids by not recording it either.
|
|
358
|
+
if (error?.code === REFUSED) {
|
|
359
|
+
console.warn(`[actions] ${name} refused: ${error.message}`);
|
|
360
|
+
return refused(error.message, "action_refused");
|
|
361
|
+
}
|
|
362
|
+
return fail(error, { "42883": "no_such_action" });
|
|
244
363
|
}
|
|
245
364
|
}
|