makersclaw 0.4.0 → 0.5.2
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/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 +5 -5
- 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 +2 -2
- 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 +121 -121
- 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/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/templates/next/files/README.md +77 -19
- package/templates/next/files/app/auto-refresh.jsx +39 -74
- package/templates/next/files/app/frame.js +197 -6
- package/templates/next/files/check/run.mjs +96 -11
- package/templates/next/files/components/ui/calendar.jsx +24 -0
- package/templates/next/files/components/ui/field.jsx +12 -3
- 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/data.js +4 -1
- package/templates/next/files/lib/db.js +18 -1
- package/templates/next/files/lib/hooks.js +52 -17
- package/templates/next/template.json +2 -2
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// browser: the server draws the notes as one list, and the day headings
|
|
15
15
|
// appear once mounted — never a mismatch between the two renders.
|
|
16
16
|
import { useEffect, useRef, useState } from "react";
|
|
17
|
-
import { Avatar, Badge, Card, Confirm, Menu, cx } from "../../ui";
|
|
17
|
+
import { Avatar, Badge, Card, Confirm, Menu, cx, toLocalDate } from "../../ui";
|
|
18
18
|
import { CommentBody, CommentComposer, CommentTime } from "../comment-box";
|
|
19
19
|
import { docToText } from "../doc-editor";
|
|
20
20
|
|
|
@@ -22,10 +22,9 @@ const nameOf = (author) => (typeof author === "string" ? author : author?.name)
|
|
|
22
22
|
const srcOf = (author) => (typeof author === "object" && author ? author.src : undefined);
|
|
23
23
|
const idOf = (author) => (typeof author === "object" && author ? author.id : undefined);
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
};
|
|
25
|
+
// A note dated with a day alone ("2026-09-28", a `date` column) files
|
|
26
|
+
// under that day wherever the reader is: local midnight, never UTC's.
|
|
27
|
+
const toMs = (v) => toLocalDate(v)?.getTime() ?? NaN;
|
|
29
28
|
|
|
30
29
|
function useMounted() {
|
|
31
30
|
const [on, setOn] = useState(false);
|
|
@@ -69,9 +69,9 @@ export function PanelBody({ panel, onChoose, sheet = false }) {
|
|
|
69
69
|
);
|
|
70
70
|
} else if (panel.loading) {
|
|
71
71
|
body = (
|
|
72
|
-
<ul aria-busy="true" aria-label={`Loading ${panel.title ?? "list"}`} className="grid">
|
|
72
|
+
<ul aria-busy="true" aria-label={`Loading ${panel.title ?? "list"}`} className="grid grid-cols-1">
|
|
73
73
|
{[0, 1, 2, 3, 4].map((i) => (
|
|
74
|
-
<li key={i} className="grid gap-2 border-b border-border px-4 py-3">
|
|
74
|
+
<li key={i} className="grid grid-cols-1 gap-2 border-b border-border px-4 py-3">
|
|
75
75
|
<Skeleton className="w-1/2" />
|
|
76
76
|
<Skeleton className="w-3/4" />
|
|
77
77
|
<Skeleton className="w-full" />
|
|
@@ -90,7 +90,7 @@ export function PanelBody({ panel, onChoose, sheet = false }) {
|
|
|
90
90
|
);
|
|
91
91
|
} else {
|
|
92
92
|
body = (
|
|
93
|
-
<ul aria-label={panel.title ?? "List"} className="grid">
|
|
93
|
+
<ul aria-label={panel.title ?? "List"} className="grid grid-cols-1">
|
|
94
94
|
{shown.map((it, i) => (
|
|
95
95
|
<li key={it.key ?? it.href ?? `${it.title}-${i}`} className="border-b border-border">
|
|
96
96
|
<Item
|
|
@@ -120,7 +120,7 @@ export function PanelBody({ panel, onChoose, sheet = false }) {
|
|
|
120
120
|
return (
|
|
121
121
|
<div className="flex min-h-0 flex-1 flex-col">
|
|
122
122
|
{panel.title || panel.actions || searchOn ? (
|
|
123
|
-
<div className="grid shrink-0 gap-3 border-b border-border p-3">
|
|
123
|
+
<div className="grid grid-cols-1 shrink-0 gap-3 border-b border-border p-3">
|
|
124
124
|
{panel.title || panel.actions ? (
|
|
125
125
|
<div className="flex min-h-9 items-center gap-2">
|
|
126
126
|
{panel.title ? <h2 className="min-w-0 flex-1 truncate text-base font-semibold">{panel.title}</h2> : null}
|
|
@@ -354,7 +354,7 @@ export function Sidebar({
|
|
|
354
354
|
<Icon name={item.icon} className="shrink-0" />
|
|
355
355
|
) : null}
|
|
356
356
|
{item.description ? (
|
|
357
|
-
<span className="grid min-w-0 flex-1 text-left leading-tight">
|
|
357
|
+
<span className="grid grid-cols-1 min-w-0 flex-1 text-left leading-tight">
|
|
358
358
|
<span className="truncate">{item.label}</span>
|
|
359
359
|
<span className="truncate text-xs text-muted-foreground">{item.description}</span>
|
|
360
360
|
</span>
|
|
@@ -420,7 +420,7 @@ export function Sidebar({
|
|
|
420
420
|
opts.sheet ? "[&>summary]:px-4" : "[&>summary]:min-h-9 [&>summary]:px-3 [&>summary]:py-1",
|
|
421
421
|
)}
|
|
422
422
|
>
|
|
423
|
-
<ul className="ml-5 grid gap-0.5 border-l border-border pl-2">
|
|
423
|
+
<ul className="ml-5 grid grid-cols-1 gap-0.5 border-l border-border pl-2">
|
|
424
424
|
{item.items.map((child) => (
|
|
425
425
|
<li key={keyOf(child)}>{leaf(child, opts)}</li>
|
|
426
426
|
))}
|
|
@@ -469,7 +469,7 @@ export function Sidebar({
|
|
|
469
469
|
function replaced(item, { sheet, where }) {
|
|
470
470
|
const headingId = `${uid}-${where}-drill`;
|
|
471
471
|
return (
|
|
472
|
-
<div className="grid gap-0.5">
|
|
472
|
+
<div className="grid grid-cols-1 gap-0.5">
|
|
473
473
|
<Button id={`${uid}-${where}-back`} variant="nav" size={sheet ? "md" : "sm"} onClick={() => closeDrill(where)}>
|
|
474
474
|
<Icon name="arrow-left" className="shrink-0" />
|
|
475
475
|
<span className="min-w-0 flex-1 truncate text-left">Back</span>
|
|
@@ -477,7 +477,7 @@ export function Sidebar({
|
|
|
477
477
|
<h2 id={headingId} className="flex h-8 items-center px-3 text-xs font-medium text-muted-foreground">
|
|
478
478
|
{item.label}
|
|
479
479
|
</h2>
|
|
480
|
-
<ul aria-labelledby={headingId} className="grid gap-0.5">
|
|
480
|
+
<ul aria-labelledby={headingId} className="grid grid-cols-1 gap-0.5">
|
|
481
481
|
{item.items.map((child) => (
|
|
482
482
|
<li key={keyOf(child)}>{leaf(child, { sheet })}</li>
|
|
483
483
|
))}
|
|
@@ -513,7 +513,7 @@ export function Sidebar({
|
|
|
513
513
|
// children stand in for the list.
|
|
514
514
|
const drilledHere = drillItem && !icons && (drillMode === "replace" || (sheet && drillMode === "panel"));
|
|
515
515
|
return (
|
|
516
|
-
<nav aria-label={label} className={cx("grid gap-3", icons ? "justify-items-center py-2" : "px-2 py-2")}>
|
|
516
|
+
<nav aria-label={label} className={cx("grid grid-cols-1 gap-3", icons ? "justify-items-center py-2" : "px-2 py-2")}>
|
|
517
517
|
{primaryAction && !drilledHere ? primary({ icons, sheet }) : null}
|
|
518
518
|
{drilledHere
|
|
519
519
|
? replaced(drillItem, { sheet, where })
|
|
@@ -524,7 +524,7 @@ export function Sidebar({
|
|
|
524
524
|
<ul
|
|
525
525
|
aria-labelledby={group.label && !icons && !group.collapsible ? headingId : undefined}
|
|
526
526
|
aria-label={group.label && !icons && group.collapsible ? group.label : undefined}
|
|
527
|
-
className={cx("grid gap-0.5", icons && "justify-items-center")}
|
|
527
|
+
className={cx("grid grid-cols-1 gap-0.5", icons && "justify-items-center")}
|
|
528
528
|
>
|
|
529
529
|
{group.items.map((item) => (
|
|
530
530
|
<li key={keyOf(item)}>{icons ? iconItem(item) : wideItem(item, { sheet, where })}</li>
|
|
@@ -546,7 +546,7 @@ export function Sidebar({
|
|
|
546
546
|
);
|
|
547
547
|
}
|
|
548
548
|
return (
|
|
549
|
-
<div key={group.key ?? group.label ?? gi} className="grid gap-0.5">
|
|
549
|
+
<div key={group.key ?? group.label ?? gi} className="grid grid-cols-1 gap-0.5">
|
|
550
550
|
{group.label && !icons ? (
|
|
551
551
|
<h2 id={headingId} className="flex h-8 items-center px-3 text-xs font-medium text-muted-foreground">
|
|
552
552
|
{group.label}
|
|
@@ -565,7 +565,7 @@ export function Sidebar({
|
|
|
565
565
|
|
|
566
566
|
function brand({ icons, side = "bottom" }) {
|
|
567
567
|
const words = (
|
|
568
|
-
<span className="grid min-w-0 flex-1 text-left leading-tight">
|
|
568
|
+
<span className="grid grid-cols-1 min-w-0 flex-1 text-left leading-tight">
|
|
569
569
|
<span className="truncate text-sm font-semibold">{title}</span>
|
|
570
570
|
{subtitle ? <span className="truncate text-xs font-normal text-muted-foreground">{subtitle}</span> : null}
|
|
571
571
|
</span>
|
|
@@ -667,7 +667,7 @@ export function Sidebar({
|
|
|
667
667
|
if (!user) return null;
|
|
668
668
|
const face = <Avatar name={user.name} src={user.src} size="sm" />;
|
|
669
669
|
const words = (
|
|
670
|
-
<span className="grid min-w-0 flex-1 text-left leading-tight">
|
|
670
|
+
<span className="grid grid-cols-1 min-w-0 flex-1 text-left leading-tight">
|
|
671
671
|
<span className="truncate text-sm font-medium">{user.name}</span>
|
|
672
672
|
{user.email ? <span className="truncate text-xs font-normal text-muted-foreground">{user.email}</span> : null}
|
|
673
673
|
</span>
|
|
@@ -711,7 +711,7 @@ export function Sidebar({
|
|
|
711
711
|
) : null;
|
|
712
712
|
if (!user && !plus && !footSwitch) return null;
|
|
713
713
|
return (
|
|
714
|
-
<div className="grid gap-1">
|
|
714
|
+
<div className="grid grid-cols-1 gap-1">
|
|
715
715
|
{footSwitch ? brand({ icons, side: "top" }) : null}
|
|
716
716
|
{user || plus ? (
|
|
717
717
|
<div className={cx("flex items-center gap-1", icons && "flex-col")}>
|
|
@@ -745,7 +745,7 @@ export function Sidebar({
|
|
|
745
745
|
<IconButton icon="x" label={`Close ${drillItem.label}`} size="sm" onClick={() => closeDrill("r")} />
|
|
746
746
|
</div>
|
|
747
747
|
<ScrollArea label={drillItem.label} className="min-h-0 flex-1" fade>
|
|
748
|
-
<ul aria-labelledby={headingId} className="grid gap-0.5 p-2">
|
|
748
|
+
<ul aria-labelledby={headingId} className="grid grid-cols-1 gap-0.5 p-2">
|
|
749
749
|
{drillItem.items.map((child, i) => (
|
|
750
750
|
<li key={keyOf(child)}>{leaf(child, { sheet: false, id: i === 0 ? `${uid}-drill-first` : undefined })}</li>
|
|
751
751
|
))}
|
|
@@ -813,7 +813,7 @@ export function Sidebar({
|
|
|
813
813
|
>
|
|
814
814
|
{ctx.mobileOpen ? (
|
|
815
815
|
sheetPanel && panel ? (
|
|
816
|
-
<div className="-mx-2 -my-2 grid gap-2">
|
|
816
|
+
<div className="-mx-2 -my-2 grid grid-cols-1 gap-2">
|
|
817
817
|
<div className="px-2 pt-2">
|
|
818
818
|
<Button variant="nav" size="md" onClick={() => setSheetPanel(false)}>
|
|
819
819
|
<Icon name="arrow-left" className="shrink-0" />
|
|
@@ -823,7 +823,7 @@ export function Sidebar({
|
|
|
823
823
|
{panelIsNode ? panel : <PanelBody panel={panel} sheet onChoose={() => ctx.setMobileOpen(false)} />}
|
|
824
824
|
</div>
|
|
825
825
|
) : (
|
|
826
|
-
<div className="-mx-2 -my-2 grid gap-2">
|
|
826
|
+
<div className="-mx-2 -my-2 grid grid-cols-1 gap-2">
|
|
827
827
|
{headerActions ? <div className="flex flex-wrap items-center gap-1 px-2">{headerActions}</div> : null}
|
|
828
828
|
{searchOpts ? <div className="px-2">{searchRow({ icons: false, sheet: true })}</div> : null}
|
|
829
829
|
{switcher?.length ? (
|
|
@@ -60,9 +60,16 @@ export function signatureRecord({ png, name, mode, signedAt }) {
|
|
|
60
60
|
return { png, name: String(name ?? "").trim(), mode, signedAt: signedAt ?? new Date().toISOString() };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
// A day alone ("2026-09-28", from a `date` column) is that day's local
|
|
64
|
+
// midnight: new Date() would read it as UTC's, the day before west of
|
|
65
|
+
// Greenwich. Written out here, not the kit's toLocalDate: these helpers
|
|
66
|
+
// import nothing, so they run anywhere.
|
|
67
|
+
const DAY = /^(\d{4})-(\d{2})-(\d{2})$/;
|
|
68
|
+
|
|
63
69
|
/** "Sep 23, 2026, 2:04 PM" in the reader's own locale. */
|
|
64
70
|
export function formatSignedAt(iso, locale) {
|
|
65
|
-
const
|
|
71
|
+
const day = typeof iso === "string" ? DAY.exec(iso) : null;
|
|
72
|
+
const d = day ? new Date(+day[1], +day[2] - 1, +day[3]) : new Date(iso);
|
|
66
73
|
if (Number.isNaN(d.getTime())) return "";
|
|
67
74
|
return d.toLocaleString(locale, { month: "short", day: "numeric", year: "numeric", hour: "numeric", minute: "2-digit" });
|
|
68
75
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// type's `editor` name. A custom type (the `types` prop) may bring its own
|
|
6
6
|
// `render(value, column, ctx)` and `Editor` component.
|
|
7
7
|
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
8
|
-
import { Avatar, AvatarGroup, Badge, Calendar, Command, Icon, Rating, cx, panelClass, usePopoverPosition } from "../../ui";
|
|
8
|
+
import { Avatar, AvatarGroup, Badge, Calendar, Command, Icon, Rating, cx, panelClass, todayString, usePopoverPosition } from "../../ui";
|
|
9
9
|
import { optionOf, optionValue } from "./types";
|
|
10
10
|
|
|
11
11
|
const asList = (v) => (Array.isArray(v) ? v : v == null || v === "" ? [] : [v]);
|
|
@@ -303,7 +303,7 @@ function DateEditor({ value, column, type, ctx, initial, today, onCommit, onCanc
|
|
|
303
303
|
<button
|
|
304
304
|
type="button"
|
|
305
305
|
className="rounded-sm px-1.5 py-1 text-xs text-muted-foreground outline-none hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
|
306
|
-
onClick={() => onCommit(today ??
|
|
306
|
+
onClick={() => onCommit(today ?? todayString(), null)}
|
|
307
307
|
>
|
|
308
308
|
Today
|
|
309
309
|
</button>
|
|
@@ -56,6 +56,7 @@ app/DESIGN.md the design, written before the code (§3)
|
|
|
56
56
|
surfaces/<key>.jsx one file per domain surface
|
|
57
57
|
surfaces.js the manifest entries (import + component when built)
|
|
58
58
|
components/*.jsx anything shared between surfaces
|
|
59
|
+
components/*-actions.js your own server actions, for a write lib/actions.js cannot make (§5)
|
|
59
60
|
components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
|
|
60
61
|
```
|
|
61
62
|
|
|
@@ -67,7 +68,7 @@ app/globals.css the workspace theme's tokens (§1) — no palette of your
|
|
|
67
68
|
app/page.jsx / → the home surface (or "No surfaces yet" while the manifest is empty)
|
|
68
69
|
app/s/[key]/page.jsx renders the surface with that key; planned ones say so
|
|
69
70
|
app/error.jsx, app/global-error.jsx, app/not-found.jsx, app/error-relay.jsx, app/auto-refresh.jsx,
|
|
70
|
-
app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its window: refresh while shown, Escape from the desk
|
|
71
|
+
app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its window: refresh while shown (a surface opts out, §7), Escape from the desk
|
|
71
72
|
components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
|
|
72
73
|
components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
|
|
73
74
|
check/pins.json the fixed files by hash; `npm run check` refuses one that differs
|
|
@@ -194,7 +195,7 @@ import { Button, IconButton, ButtonGroup, ButtonGroupSeparator, Toggle, ToggleGr
|
|
|
194
195
|
Field, Input, TimeInput, Textarea, Select, Switch, Checkbox, RadioGroup, Radio, Rating,
|
|
195
196
|
InputGroup, PasswordInput, InputOTP, Slider, Image, FileInput, FileDrop, fileMatches, Combobox, Command, CommandDialog, Calendar, DatePicker, DateInput, DATE_PRESETS,
|
|
196
197
|
parseDate, isDateString, toDateString, todayString, addDays, addMonths,
|
|
197
|
-
startOfMonth, endOfMonth, dayOfWeek, formatDate, firstDayOfWeek,
|
|
198
|
+
startOfMonth, endOfMonth, dayOfWeek, formatDate, toLocalDate, firstDayOfWeek,
|
|
198
199
|
Card, Notice, Badge, Kicker, Empty, Item, ItemGroup, Avatar, AvatarGroup,
|
|
199
200
|
Sheet, Confirm, Tabs, TabPanel, Menu, ContextMenu, Popover, Tooltip, HoverCard,
|
|
200
201
|
Breadcrumb, Pagination, Collapsible, Accordion, Carousel, ScrollArea,
|
|
@@ -218,8 +219,10 @@ import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNoti
|
|
|
218
219
|
`IconButton` for an icon alone (`label` required). `loading` while its
|
|
219
220
|
action runs: a spinner, the same width, `aria-busy`, disabled.
|
|
220
221
|
- `Field` wraps `Input`, `Textarea`, `Select`, `Switch`, `Checkbox` with
|
|
221
|
-
its label, hint and error. `
|
|
222
|
-
|
|
222
|
+
its label, hint and error. An `Input` is as wide as its column; a width
|
|
223
|
+
in its `className` (`w-24`, `w-auto`) sets its own. `Select` with
|
|
224
|
+
empty `options` renders a muted line, never a disabled dropdown.
|
|
225
|
+
`TimeInput` — a time of day
|
|
223
226
|
("HH:MM"; `min max step`), the kit's clock in place of the browser's.
|
|
224
227
|
- `Rating` — stars: read-only `value` of `max` (halves drawn, "4.5 out
|
|
225
228
|
of 5" to a reader), or with `onChange` a radio-group input; `size`, `label`.
|
|
@@ -246,8 +249,10 @@ import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNoti
|
|
|
246
249
|
- `Sheet` — the one overlay, a rail beside the work (`side="end"`), a
|
|
247
250
|
navigation drawer (`start`), a dialog (`center`) or a bottom sheet; on a narrow window every side
|
|
248
251
|
becomes a bottom sheet; `bare` + `label` for a panel that draws its own
|
|
249
|
-
top (no header, flush body). `
|
|
250
|
-
|
|
252
|
+
top (no header, flush body). A field inside with `autoFocus` takes the
|
|
253
|
+
focus once it opens (since 0.5.0; before, Close had it). `Confirm` is
|
|
254
|
+
the "delete this?" case; `consequences`, `requireText` and
|
|
255
|
+
`acknowledge` hold it until met.
|
|
251
256
|
- `Image src alt ratio fit rounded` — a picture of a thing (thumbnail,
|
|
252
257
|
cover): Skeleton while it loads, a glyph when it fails, lazy, `alt`
|
|
253
258
|
required. Never a raw `<img>`; a face is an `Avatar`.
|
|
@@ -280,7 +285,13 @@ import { RecordSwitcher, LiveEmpty, LiveWork, liveWords, useFixture, FixtureNoti
|
|
|
280
285
|
`DateInput` (a typed date with the Calendar a button away;
|
|
281
286
|
`clearable` for an ×). Dates are
|
|
282
287
|
`"YYYY-MM-DD"` strings in and out — never a Date — and `addDays`,
|
|
283
|
-
`addMonths`, `formatDate`… do the arithmetic.
|
|
288
|
+
`addMonths`, `formatDate`… do the arithmetic. A `date` column reads
|
|
289
|
+
as that same string (§4), so a row's day goes into a `DatePicker` and
|
|
290
|
+
back unchanged. Never `new Date("2026-09-28")`: that is UTC's
|
|
291
|
+
midnight, the 27th west of Greenwich, and a form that saves what it
|
|
292
|
+
shows moves the day back. Showing a day beside times (a due date by
|
|
293
|
+
a sent-at), `toLocalDate(value)` gives a Date at that day's LOCAL
|
|
294
|
+
midnight (a timestamp or a Date passes through).
|
|
284
295
|
- `RadioGroup` (`options` or `<Radio>` children, each with a
|
|
285
296
|
`description`; `variant` list · card; `orientation`; in a `Field` with
|
|
286
297
|
the same `id`), `Toggle` (`pressed` + `onPressedChange`, aria-pressed),
|
|
@@ -506,7 +517,14 @@ surfaces.js server files only (entries import components)
|
|
|
506
517
|
```
|
|
507
518
|
|
|
508
519
|
Pass plain data across the boundary — strings, numbers, arrays, objects.
|
|
509
|
-
`plain(row)` already turns
|
|
520
|
+
`plain(row)` already turns a timestamp into its ISO string. A `date`
|
|
521
|
+
column is its day, `"2026-09-28"`: text, the same day for every reader,
|
|
522
|
+
from `loadRows` and from `query()` alike (`lib/db.js` reads it so). Up to
|
|
523
|
+
0.4.x `query()` gave a Date at the server's midnight and the reads an ISO
|
|
524
|
+
timestamp of it, a day off on either side of UTC. A server file that
|
|
525
|
+
called a Date method on a `date` value now holds the day's string:
|
|
526
|
+
compare days as strings (`"2026-09-28" < "2026-10-01"`), and build a
|
|
527
|
+
Date only where a sum needs one. A client component can
|
|
510
528
|
receive a server action as a prop, and nothing else that is a function.
|
|
511
529
|
|
|
512
530
|
A surface file that uses hooks or event handlers starts with `"use
|
|
@@ -636,7 +654,8 @@ const { counts } = await countBy({ source: "public.investors", column: "stage" }
|
|
|
636
654
|
const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
|
|
637
655
|
```
|
|
638
656
|
|
|
639
|
-
Helpers: `plain(row)` (
|
|
657
|
+
Helpers: `plain(row)` (timestamps → ISO, buffers → base64; a `date` is
|
|
658
|
+
already its `"YYYY-MM-DD"`), `orderClause(orderBy,
|
|
640
659
|
fallback)` (validated `ORDER BY` text), `markSample(sample, idColumn)`
|
|
641
660
|
(stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
|
|
642
661
|
key of the request, from `x-claw-surface`, unless one is passed).
|
|
@@ -661,6 +680,13 @@ saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn =
|
|
|
661
680
|
when missing — a plain `key text, value text` table is enough. JSON mode
|
|
662
681
|
sets `key` inside the jsonb `column` of row `id`.
|
|
663
682
|
|
|
683
|
+
Every update (`updateRow`, `moveRow`, `saveSetting`) sets `updated_at =
|
|
684
|
+
now()` when the table has an `updated_at` timestamp it can write (since
|
|
685
|
+
0.5.0; before, only a trigger of the app's own kept it). A patch that
|
|
686
|
+
sets `updated_at` itself wins, and a column of that name that is text, a
|
|
687
|
+
number or a view's computed value is left alone. An insert leaves it to
|
|
688
|
+
the column's default: `updated_at timestamptz not null default now()`.
|
|
689
|
+
|
|
664
690
|
```js
|
|
665
691
|
"use client";
|
|
666
692
|
import { moveRow } from "../lib/actions";
|
|
@@ -668,8 +694,15 @@ const r = await moveRow({ source: "public.investors", id, stateColumn: "stage",
|
|
|
668
694
|
if (!r.ok) setError(r.error);
|
|
669
695
|
```
|
|
670
696
|
|
|
671
|
-
Never write raw SQL from a surface.
|
|
672
|
-
|
|
697
|
+
Never write raw SQL from a surface. `lib/*.js` is fixed (`npm run check`
|
|
698
|
+
refuses an edit, and `makersclaw update` lays the file again), so a write
|
|
699
|
+
these cannot make goes in a server-action file of the app's own:
|
|
700
|
+
`"use server"` on its first line, beside the components that call it
|
|
701
|
+
(`components/deal-actions.js`). There it composes the actions above, or
|
|
702
|
+
runs its UPDATE or DELETE through `query()` from `lib/db.js`, records it
|
|
703
|
+
with `logActivity()` and ends with `revalidatePath("/", "layout")`, as
|
|
704
|
+
`lib/actions.js` does. A new row still goes through `insertRow`: the
|
|
705
|
+
check refuses a raw `INSERT INTO` anywhere outside `lib/`.
|
|
673
706
|
|
|
674
707
|
### Platform — `lib/platform.js` (browser)
|
|
675
708
|
|
|
@@ -824,7 +857,11 @@ error, code }` — `needs_answer` while the thread waits on an answer in
|
|
|
824
857
|
the column, `queue_full`, `unknown_job`, `no_credits`, …; the panel
|
|
825
858
|
shows the error). The hook then follows the request — every 2.5 s while
|
|
826
859
|
the App tab is on screen — until it ends. On the 0.3.x path it resolves
|
|
827
|
-
when the job ends, as it always did.
|
|
860
|
+
when the job ends, as it always did. React's development double mount
|
|
861
|
+
(and a fast refresh) cleans the hook up and mounts it again: it follows
|
|
862
|
+
a live request on from there, and an ask it cut off before an answer
|
|
863
|
+
came closes the panel — neither done nor failed. A job started from an
|
|
864
|
+
effect runs twice there; start it from a press.
|
|
828
865
|
|
|
829
866
|
What it holds, for the panel:
|
|
830
867
|
|
|
@@ -993,7 +1030,8 @@ surface; `--static` skips the build and the smoke. Then, by eye:
|
|
|
993
1030
|
- Error state shows the text from `{ error }`.
|
|
994
1031
|
- Keyboard focus visible on every control.
|
|
995
1032
|
- Numbers tabular (`data-nums`).
|
|
996
|
-
- Every write
|
|
1033
|
+
- Every write is a server action — `lib/actions.js`, or the app's own
|
|
1034
|
+
`"use server"` file (§5) — never raw SQL in a surface.
|
|
997
1035
|
- Every surface reachable from the nav.
|
|
998
1036
|
|
|
999
1037
|
## 7. Run
|
|
@@ -1018,9 +1056,29 @@ state; a 5xx carries the log tail. Fix it before moving on — the founder
|
|
|
1018
1056
|
is watching the preview.
|
|
1019
1057
|
|
|
1020
1058
|
Every page is `dynamic = "force-dynamic"`: nothing is prerendered, so a
|
|
1021
|
-
build never needs the database. `app/auto-refresh.jsx` re-renders
|
|
1022
|
-
the server
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1059
|
+
build never needs the database. `app/auto-refresh.jsx` re-renders the
|
|
1060
|
+
page from the server while it is shown, so the agent's writes appear:
|
|
1061
|
+
one refresh at a time, 5 s after the last one landed plus what that one
|
|
1062
|
+
took (a minute at most), and only while the App tab is on screen in a
|
|
1063
|
+
visible browser tab (hidden, it asks nothing; back, it refreshes at
|
|
1064
|
+
once), nobody is typing (never while a sheet is open, a field they typed
|
|
1065
|
+
in holds focus, or within 10 s of a keystroke), and the app answers
|
|
1066
|
+
`/claw-alive` (a fixed route) with a 204.
|
|
1067
|
+
|
|
1068
|
+
**A surface that must not be refreshed opts out with
|
|
1069
|
+
`data-claw-refresh="off"` on its root element**: one whose rows only its
|
|
1070
|
+
own writes change, or a table too big to send every few seconds. While
|
|
1071
|
+
any element on the page carries it, nothing is refreshed and nothing is
|
|
1072
|
+
probed; the surface's own writes still re-render it (`lib/actions.js`
|
|
1073
|
+
revalidates), and so does every navigation. Set it from state to pause
|
|
1074
|
+
only while it matters: `data-claw-refresh={importing ? "off" : undefined}`.
|
|
1075
|
+
|
|
1076
|
+
**A page left open stops refreshing after 30 minutes with no sign of
|
|
1077
|
+
anyone** in the app — a key, the pointer, a touch, a wheel, focus. The
|
|
1078
|
+
platform stops an app nobody used for 30 minutes, and every refresh is a
|
|
1079
|
+
use, so an open tab nobody looked at kept its app running all night. The
|
|
1080
|
+
next sign of someone refreshes at once and the refresh goes on; so does
|
|
1081
|
+
coming back to the tab. Nothing on the page says so: the first touch
|
|
1082
|
+
brings it up to date.
|
|
1083
|
+
|
|
1084
|
+
The founder reads "artifact", "app", "agent", "automation" — nothing else.
|
|
@@ -1,88 +1,53 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useEffect } from "react";
|
|
3
|
+
import { useEffect, useRef, useTransition } from "react";
|
|
4
4
|
import { useRouter } from "next/navigation";
|
|
5
|
-
import {
|
|
5
|
+
import { refreshLoop } from "./frame";
|
|
6
6
|
|
|
7
7
|
// Rows change under this page from outside it — the agent writes them
|
|
8
|
-
// while the founder watches. Re-render from the server
|
|
9
|
-
// router.refresh() keeps form state and scroll, a reload would not.
|
|
8
|
+
// while the founder watches. Re-render from the server while the page is
|
|
9
|
+
// shown; router.refresh() keeps form state and scroll, a reload would not.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
// desk it is always "visible": the window keeps the App tab mounted,
|
|
13
|
-
// hidden, behind its other tabs, so an app refreshed every 5 s behind
|
|
14
|
-
// Runs and Settings, and refreshed under the founder's hands mid-word.
|
|
15
|
-
// One 502 on such a refresh (the app restarting) made Next load the
|
|
16
|
-
// whole frame again and the half-typed field was gone (landing-page-
|
|
17
|
-
// reviewer, okr-tracker, 2026-09-24). Now a tick refreshes only when:
|
|
11
|
+
// The rules are app/frame.js's (refreshLoop, and the reasons with each):
|
|
18
12
|
//
|
|
19
|
-
// - the tab is visible AND the App tab is on screen
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
13
|
+
// - only while the tab is visible AND the App tab is on screen: no timer
|
|
14
|
+
// at all otherwise, and coming back refreshes at once;
|
|
15
|
+
// - only while someone is there: after 30 minutes with no key, pointer,
|
|
16
|
+
// touch, wheel or focus in the app it stops, so the platform's idle
|
|
17
|
+
// sweep can stop an app left open; the next sign refreshes at once;
|
|
18
|
+
// - only while the founder is not typing and no dialog is open
|
|
19
|
+
// (isEditing), and never while a surface has opted out with
|
|
20
|
+
// data-claw-refresh="off" on its root;
|
|
21
|
+
// - only after app/claw-alive/route.js answers 204 within 4 s: while
|
|
22
|
+
// the app is down, restarting or unreachable the page stays as it is,
|
|
23
|
+
// never reloaded;
|
|
24
|
+
// - one refresh at a time. It runs in a transition, and the next tick is
|
|
25
|
+
// timed only once `isPending` is false again: the interval plus what
|
|
26
|
+
// the last one took, so a slow render is never cut off by the next and
|
|
27
|
+
// a busy stack is asked less often. Until 0.5.0 a refresh fired every
|
|
28
|
+
// 5 s whatever was in flight.
|
|
33
29
|
export default function AutoRefresh() {
|
|
34
30
|
const router = useRouter();
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
let lastInput = -Infinity;
|
|
38
|
-
let typed = null; // the field the last `input` event came from
|
|
39
|
-
let asking = false;
|
|
40
|
-
let stopped = false;
|
|
41
|
-
|
|
42
|
-
const ready = () =>
|
|
43
|
-
shouldRefresh({
|
|
44
|
-
visible: document.visibilityState === "visible",
|
|
45
|
-
onScreen,
|
|
46
|
-
editing: isEditing(document, typed),
|
|
47
|
-
quietFor: Date.now() - lastInput,
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
const tick = async () => {
|
|
51
|
-
if (asking || !ready()) return;
|
|
52
|
-
asking = true;
|
|
53
|
-
const up = await answering(probeUrl(document));
|
|
54
|
-
asking = false;
|
|
55
|
-
if (up && !stopped && ready()) router.refresh();
|
|
56
|
-
};
|
|
31
|
+
const [pending, startTransition] = useTransition();
|
|
32
|
+
const landed = useRef([]); // the loop's waits for the refresh in flight
|
|
57
33
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
let observer = null;
|
|
67
|
-
if (typeof IntersectionObserver === "function") {
|
|
68
|
-
observer = new IntersectionObserver((entries) => {
|
|
69
|
-
const was = onScreen;
|
|
70
|
-
onScreen = entries[entries.length - 1].isIntersecting;
|
|
71
|
-
if (onScreen && !was) tick();
|
|
72
|
-
});
|
|
73
|
-
observer.observe(document.documentElement);
|
|
74
|
-
}
|
|
34
|
+
// isPending turns false when the refresh's new tree has rendered: that
|
|
35
|
+
// is the refresh landed.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (pending) return;
|
|
38
|
+
for (const done of landed.current.splice(0)) done();
|
|
39
|
+
}, [pending]);
|
|
75
40
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const loop = refreshLoop({
|
|
43
|
+
doc: document,
|
|
44
|
+
refresh: () =>
|
|
45
|
+
new Promise((done) => {
|
|
46
|
+
landed.current.push(done);
|
|
47
|
+
startTransition(() => router.refresh());
|
|
48
|
+
}),
|
|
49
|
+
});
|
|
50
|
+
return () => loop.stop();
|
|
86
51
|
}, [router]);
|
|
87
52
|
return null;
|
|
88
53
|
}
|