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
|
@@ -8,7 +8,8 @@ import { Button, Card, Empty, Field, Icon, Skeleton, Textarea, cx } from "../../
|
|
|
8
8
|
// keeps the four rules a hand-drawn panel breaks: it is not there when
|
|
9
9
|
// nothing runs, it shows each step's label (the founder's phrase, never
|
|
10
10
|
// the tool's name), the agent's text is its account and not the
|
|
11
|
-
// deliverable (the rows are), and Stop / Close are always one press away
|
|
11
|
+
// deliverable (the rows are), and Stop / Close are always one press away
|
|
12
|
+
// (Answer in Stop's place while the agent waits on the founder).
|
|
12
13
|
//
|
|
13
14
|
// const job = useJob();
|
|
14
15
|
// <Button onClick={() => job.start("Draft a launch plan for Rowhouse")}>Generate a launch plan</Button>
|
|
@@ -24,10 +25,21 @@ import { Button, Card, Empty, Field, Icon, Skeleton, Textarea, cx } from "../../
|
|
|
24
25
|
// options={<Field …><Select …/></Field>}
|
|
25
26
|
// onGenerate={(prompt) => job.start(`Make a product photo: ${prompt}. Size ${size}.`)}
|
|
26
27
|
// results={photos.map((p) => <PhotoCard key={p.id} photo={p} />)} />
|
|
28
|
+
//
|
|
29
|
+
// Since 0.4.0 a job is a request, and its conversation lives in the
|
|
30
|
+
// desk's agent column: the agent's questions and the founder's yes are
|
|
31
|
+
// there, not here. So the panel reads two more states off the job, when
|
|
32
|
+
// the job has them: needsYou (the agent waits on the founder — the
|
|
33
|
+
// heading says so and Answer, in Stop's place, opens the question in the
|
|
34
|
+
// column) and queued (one job per list runs at a time — the heading names
|
|
35
|
+
// the job it waits behind). A 0.3.x job object has neither and draws
|
|
36
|
+
// exactly as it always did.
|
|
27
37
|
|
|
28
38
|
function StepMark({ status }) {
|
|
29
39
|
if (status === "done") return <Icon name="check" size={14} className="shrink-0 text-success" />;
|
|
30
40
|
if (status === "failed") return <Icon name="warning" size={14} className="shrink-0 text-destructive" />;
|
|
41
|
+
if (status === "needs") return <Icon name="circle-help" size={14} className="shrink-0 text-warning" />;
|
|
42
|
+
if (status === "queued") return <Icon name="clock" size={14} className="shrink-0 text-muted-foreground" />;
|
|
31
43
|
return <span aria-hidden="true" className="mx-[3px] size-2 shrink-0 animate-pulse rounded-full bg-primary" />;
|
|
32
44
|
}
|
|
33
45
|
|
|
@@ -36,11 +48,17 @@ export function JobPanel({ variant = "panel", ...props }) {
|
|
|
36
48
|
return <Panel {...props} />;
|
|
37
49
|
}
|
|
38
50
|
|
|
51
|
+
// Waiting on the founder or on another job is still a live job: not over,
|
|
52
|
+
// only paused. useJob counts both as running; an object of its own shape
|
|
53
|
+
// that sets only the flag gets the same panel.
|
|
54
|
+
const isLive = (job) => Boolean(job?.running || job?.needsYou || job?.queued);
|
|
55
|
+
|
|
39
56
|
function Panel({
|
|
40
57
|
job,
|
|
41
58
|
title = "Working on it",
|
|
42
59
|
doneTitle = "Done",
|
|
43
60
|
failedTitle = "It stopped",
|
|
61
|
+
needsTitle = "Needs your answer",
|
|
44
62
|
onRetry,
|
|
45
63
|
autoClose = 6000,
|
|
46
64
|
maxSteps = 6,
|
|
@@ -48,7 +66,9 @@ function Panel({
|
|
|
48
66
|
className,
|
|
49
67
|
}) {
|
|
50
68
|
const textRef = useRef(null);
|
|
51
|
-
const
|
|
69
|
+
const needsYou = Boolean(job?.needsYou);
|
|
70
|
+
const queued = !needsYou && Boolean(job?.queued);
|
|
71
|
+
const running = isLive(job);
|
|
52
72
|
const failed = !running && Boolean(job?.error);
|
|
53
73
|
const done = !running && !failed && Boolean(job?.finishedAt);
|
|
54
74
|
|
|
@@ -67,23 +87,43 @@ function Panel({
|
|
|
67
87
|
|
|
68
88
|
if (!job || !(running || failed || done)) return null;
|
|
69
89
|
|
|
70
|
-
|
|
90
|
+
// What the job waits on, if anything: its mark replaces the pulse.
|
|
91
|
+
const waiting = needsYou ? "needs" : queued ? "queued" : null;
|
|
92
|
+
const heading = needsYou
|
|
93
|
+
? needsTitle
|
|
94
|
+
: queued
|
|
95
|
+
? `Queued · after ${job.queuedAfter?.after_title ?? "the job before it"}`
|
|
96
|
+
: running
|
|
97
|
+
? title
|
|
98
|
+
: failed
|
|
99
|
+
? failedTitle
|
|
100
|
+
: doneTitle;
|
|
101
|
+
// A queued request's one step is its line, and its line is this
|
|
102
|
+
// heading again: say it once.
|
|
103
|
+
const steps = (job.steps ?? []).filter((step) => !(queued && step.label === heading));
|
|
71
104
|
const hidden = Math.max(0, steps.length - maxSteps);
|
|
72
105
|
const visible = steps.slice(hidden);
|
|
73
|
-
|
|
106
|
+
// The question is in the desk's agent column; open() takes the founder
|
|
107
|
+
// there. Without it there is nowhere to point, and Stop stays.
|
|
108
|
+
const answer = needsYou && typeof job.open === "function";
|
|
74
109
|
|
|
75
110
|
return (
|
|
76
111
|
<section
|
|
77
112
|
aria-label={heading}
|
|
78
|
-
className={cx(
|
|
113
|
+
className={cx(
|
|
114
|
+
"rounded-lg border bg-card p-4 text-card-foreground",
|
|
115
|
+
failed ? "border-destructive/50" : needsYou ? "border-warning/50" : "border-border",
|
|
116
|
+
className,
|
|
117
|
+
)}
|
|
79
118
|
>
|
|
80
119
|
<div className="flex items-start justify-between gap-3">
|
|
81
120
|
<div className="min-w-0">
|
|
82
121
|
<p aria-live="polite" className="flex items-center gap-2 text-sm font-semibold">
|
|
83
|
-
{running ?
|
|
122
|
+
<StepMark status={waiting ?? (running ? "running" : failed ? "failed" : "done")} />
|
|
84
123
|
<span className="min-w-0">{heading}</span>
|
|
85
124
|
</p>
|
|
86
125
|
{failed ? <p className="mt-1 text-sm text-muted-foreground">{job.error}</p> : null}
|
|
126
|
+
{needsYou ? <p className="mt-1 text-sm text-muted-foreground">The question is in the agent column.</p> : null}
|
|
87
127
|
</div>
|
|
88
128
|
<div className="flex shrink-0 gap-2">
|
|
89
129
|
{failed && onRetry ? (
|
|
@@ -91,7 +131,11 @@ function Panel({
|
|
|
91
131
|
Try again
|
|
92
132
|
</Button>
|
|
93
133
|
) : null}
|
|
94
|
-
{
|
|
134
|
+
{answer ? (
|
|
135
|
+
<Button size="sm" variant="secondary" onClick={() => job.open()}>
|
|
136
|
+
Answer
|
|
137
|
+
</Button>
|
|
138
|
+
) : running ? (
|
|
95
139
|
<Button size="sm" variant="quiet" onClick={() => job.cancel?.()}>
|
|
96
140
|
Stop
|
|
97
141
|
</Button>
|
|
@@ -107,12 +151,14 @@ function Panel({
|
|
|
107
151
|
{hidden > 0 ? <li className="text-xs text-muted-foreground">{hidden} earlier {hidden === 1 ? "step" : "steps"}</li> : null}
|
|
108
152
|
{visible.map((step, i) => (
|
|
109
153
|
<li key={`${hidden + i}-${step.name}`} className="flex items-center gap-2">
|
|
110
|
-
<StepMark status={step.status} />
|
|
154
|
+
<StepMark status={waiting && step.status === "running" ? waiting : step.status} />
|
|
111
155
|
<span className={step.status === "running" ? "text-foreground" : "text-muted-foreground"}>{step.label ?? "Working"}</span>
|
|
112
156
|
</li>
|
|
113
157
|
))}
|
|
114
158
|
</ol>
|
|
115
159
|
) : null}
|
|
160
|
+
{/* The agent's account, on the 0.3.x path. A request's text is empty
|
|
161
|
+
(its account is in the column), so the area is not drawn. */}
|
|
116
162
|
{showText && job.text ? (
|
|
117
163
|
<div
|
|
118
164
|
ref={textRef}
|
|
@@ -159,6 +205,7 @@ function Generator({
|
|
|
159
205
|
title = "Working on it",
|
|
160
206
|
doneTitle = "Done",
|
|
161
207
|
failedTitle = "It stopped",
|
|
208
|
+
needsTitle = "Needs your answer",
|
|
162
209
|
onRetry,
|
|
163
210
|
autoClose = 6000,
|
|
164
211
|
maxSteps = 6,
|
|
@@ -175,7 +222,7 @@ function Generator({
|
|
|
175
222
|
if (controlled === undefined) setOwn(v);
|
|
176
223
|
onPromptChange?.(v);
|
|
177
224
|
};
|
|
178
|
-
const running =
|
|
225
|
+
const running = isLive(job);
|
|
179
226
|
const canGenerate = !disabled && !running && !starting && (canGenerateEmpty || value.trim().length > 0);
|
|
180
227
|
|
|
181
228
|
async function generate(text = value) {
|
|
@@ -192,8 +239,9 @@ function Generator({
|
|
|
192
239
|
setStarting(false);
|
|
193
240
|
if (r && r.ok === false && !r.aborted) setError(r.error ?? "It did not start");
|
|
194
241
|
}
|
|
195
|
-
// useJob's start resolves when the
|
|
196
|
-
//
|
|
242
|
+
// useJob's start resolves when the ask is taken (on a 0.3.x claw, when
|
|
243
|
+
// the job ends); a failure it reports is already the panel's error, so
|
|
244
|
+
// the field says only what the panel does not.
|
|
197
245
|
const fieldError = error && !job?.error ? error : null;
|
|
198
246
|
|
|
199
247
|
const cols = { 1: "grid-cols-1", 2: "grid-cols-1 sm:grid-cols-2", 3: "grid-cols-1 sm:grid-cols-2 lg:grid-cols-3", 4: "grid-cols-2 lg:grid-cols-4" };
|
|
@@ -236,6 +284,7 @@ function Generator({
|
|
|
236
284
|
title={title}
|
|
237
285
|
doneTitle={doneTitle}
|
|
238
286
|
failedTitle={failedTitle}
|
|
287
|
+
needsTitle={needsTitle}
|
|
239
288
|
onRetry={onRetry ?? (lastPrompt.current || canGenerateEmpty ? () => generate(lastPrompt.current) : undefined)}
|
|
240
289
|
autoClose={autoClose}
|
|
241
290
|
maxSteps={maxSteps}
|
|
@@ -14,6 +14,11 @@ reader, and a quick way to jump to a topic.
|
|
|
14
14
|
It is built on `flow-canvas` (which draws it, pans and zooms): this block
|
|
15
15
|
owns the tree, the layout and the keys.
|
|
16
16
|
|
|
17
|
+
A map opens fitted to its view, but never smaller than its words can be
|
|
18
|
+
read: 79%, where its smallest words (14px) draw at the kit's smallest
|
|
19
|
+
text size (11px). A map too big for that — most maps on a phone — opens
|
|
20
|
+
at 79% centred on its centre topic; ⇧1 still fits the whole of it.
|
|
21
|
+
|
|
17
22
|
```jsx
|
|
18
23
|
import { MindMap } from "../components/blocks/mind-map"; // a Makersclaw app ("./blocks/mind-map" from components/)
|
|
19
24
|
import { MindMap } from "@/components/makersclaw/blocks/mind-map"; // a shadcn project
|
|
@@ -64,7 +69,9 @@ string, or nothing (a fresh map). The component runs it on `value`.
|
|
|
64
69
|
|
|
65
70
|
- `value` — the map, controlled. A new `value` that is not what the map
|
|
66
71
|
last sent through `onChange` replaces it (loading another record); that
|
|
67
|
-
replacement can be undone
|
|
72
|
+
replacement can be undone, and when it is another map (a new centre, or
|
|
73
|
+
other main branches) the view fits it, as it fits a first one. Pass
|
|
74
|
+
back exactly what `onChange` gave you.
|
|
68
75
|
- `defaultValue` — the starting map, uncontrolled. With neither, a centre
|
|
69
76
|
topic called "Central topic" and the first-run card.
|
|
70
77
|
- `onChange(map)` — the whole tree after a change settles (debounced by
|
|
@@ -114,7 +121,7 @@ With a topic selected (click one, or press an arrow):
|
|
|
114
121
|
| ⌘Z / ⇧⌘Z | undo / redo |
|
|
115
122
|
| Home | the centre |
|
|
116
123
|
| Esc | clear the selection |
|
|
117
|
-
| ⇧1 | fit the map to the screen |
|
|
124
|
+
| ⇧1 | fit the whole map to the screen, however small |
|
|
118
125
|
| ? | the shortcuts sheet |
|
|
119
126
|
|
|
120
127
|
While typing a topic: **Enter** finishes it and starts the next one (on
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
mindMapToText,
|
|
34
34
|
moveTopic,
|
|
35
35
|
normalizeMap,
|
|
36
|
+
openingView,
|
|
36
37
|
outdentTopic,
|
|
37
38
|
outlineToTopics,
|
|
38
39
|
parentOf,
|
|
@@ -49,14 +50,20 @@ const EXTRA_COLORS = ["warning", "success", "info", "destructive"];
|
|
|
49
50
|
const isMac = () => typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent || "");
|
|
50
51
|
|
|
51
52
|
// Measures words in the map's own font (a canvas's measureText), so a
|
|
52
|
-
// topic's box fits its words exactly. Until the
|
|
53
|
-
// server
|
|
54
|
-
//
|
|
53
|
+
// topic's box fits its words exactly. Until the map is mounted, a guess —
|
|
54
|
+
// on the server AND in the browser's first render, which must lay the
|
|
55
|
+
// map out exactly as the server did: measured there, the topics landed
|
|
56
|
+
// elsewhere, the canvas's placeholder listed them in another order, and
|
|
57
|
+
// React threw a hydration error on every load of a saved map
|
|
58
|
+
// (makersclaw-store BLOCKS.md, mind-maps 0.1.0). Measured once mounted
|
|
59
|
+
// (the font of the element the map sits in, not the body's) and again
|
|
60
|
+
// when web fonts finish loading.
|
|
55
61
|
function useMeasure(hostRef) {
|
|
56
62
|
const [tick, setTick] = useState(0);
|
|
57
|
-
const r = useRef({ ctx: null, family: null, cache: new Map() });
|
|
63
|
+
const r = useRef({ ctx: null, family: null, cache: new Map(), mounted: false });
|
|
58
64
|
useEffect(() => {
|
|
59
65
|
let live = true;
|
|
66
|
+
r.current.mounted = true;
|
|
60
67
|
const again = () => {
|
|
61
68
|
if (!live) return;
|
|
62
69
|
r.current.cache.clear();
|
|
@@ -73,8 +80,8 @@ function useMeasure(hostRef) {
|
|
|
73
80
|
}, []);
|
|
74
81
|
return useCallback(
|
|
75
82
|
(text, style) => {
|
|
76
|
-
if (typeof document === "undefined") return estimateText(text, style);
|
|
77
83
|
const c = r.current;
|
|
84
|
+
if (typeof document === "undefined" || !c.mounted) return estimateText(text, style);
|
|
78
85
|
if (!c.ctx) c.ctx = document.createElement("canvas").getContext("2d");
|
|
79
86
|
if (!c.ctx) return estimateText(text, style);
|
|
80
87
|
if (!c.family) {
|
|
@@ -111,6 +118,15 @@ function useCoarsePointer() {
|
|
|
111
118
|
return coarse;
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
/** The same map, as far as the view goes: the same centre topic and the
|
|
122
|
+
* same main branches (in any order). */
|
|
123
|
+
export function sameBranches(a, b) {
|
|
124
|
+
if (!a || !b || a.id !== b.id) return false;
|
|
125
|
+
const ids = new Set((a.children ?? []).map((c) => c.id));
|
|
126
|
+
const next = b.children ?? [];
|
|
127
|
+
return next.length === ids.size && next.every((c) => ids.has(c.id));
|
|
128
|
+
}
|
|
129
|
+
|
|
114
130
|
async function settle(fn, ...args) {
|
|
115
131
|
try {
|
|
116
132
|
const res = await fn(...args);
|
|
@@ -217,24 +233,47 @@ export function MindMap({
|
|
|
217
233
|
};
|
|
218
234
|
|
|
219
235
|
// Controlled: a `value` that is not our own echo replaces the map (and
|
|
220
|
-
// can be undone).
|
|
236
|
+
// can be undone). One that is another map — a record loaded, the
|
|
237
|
+
// agent's first map over the starter — is fitted to the view like a
|
|
238
|
+
// first one: until 0.5.0 it stayed at the old map's zoom and place, and
|
|
239
|
+
// mind-maps remounted the block to get a fit. The same map back from a
|
|
240
|
+
// save (its branches unchanged) keeps the view where it is.
|
|
221
241
|
const lastProp = useRef(value);
|
|
242
|
+
const [refit, setRefit] = useState(0);
|
|
222
243
|
useEffect(() => {
|
|
223
244
|
if (value === undefined || value === lastProp.current) return;
|
|
224
245
|
lastProp.current = value;
|
|
225
246
|
if (value === lastEmitted.current) return;
|
|
226
247
|
const next = normalizeMap(value);
|
|
227
|
-
|
|
248
|
+
const before = treeRef.current;
|
|
249
|
+
setHist({ past: [...histRef.current.past, before].slice(-HISTORY), future: [] });
|
|
228
250
|
setTree(next);
|
|
229
251
|
if (selectedRef.current && !findTopic(next, selectedRef.current)) select(null);
|
|
230
252
|
if (editingRef.current) setEditing(null);
|
|
253
|
+
if (!sameBranches(before, next)) setRefit((n) => n + 1);
|
|
231
254
|
}, [value, select]);
|
|
232
255
|
|
|
233
256
|
// ── what the canvas draws ────────────────────────────────────────────
|
|
234
257
|
const board = useMemo(() => mindMapToBoard(tree, { measure, colors }), [tree, measure, colors]);
|
|
235
258
|
const sizes = useMemo(() => new Map(board.nodes.map((n) => [n.id, n])), [board]);
|
|
259
|
+
const boardRef = useRef(board);
|
|
260
|
+
boardRef.current = board;
|
|
261
|
+
|
|
262
|
+
// Open a map fitted to the view, but never smaller than its words can be
|
|
263
|
+
// read: a map too big for that opens at READABLE_ZOOM on its centre
|
|
264
|
+
// topic (openingView). ⇧1 and "Fit to screen" still fit the whole map.
|
|
265
|
+
const fitOpen = useCallback(
|
|
266
|
+
(opts) => {
|
|
267
|
+
const api = canvas.current;
|
|
268
|
+
if (!api) return;
|
|
269
|
+
const pane = boxRef.current?.querySelector(".react-flow") ?? boxRef.current;
|
|
270
|
+
api.fitView({ ...opts, ...openingView(boardRef.current.nodes, { width: pane?.clientWidth, height: pane?.clientHeight, rootId: treeRef.current.id }) });
|
|
271
|
+
},
|
|
272
|
+
[canvas],
|
|
273
|
+
);
|
|
236
274
|
|
|
237
|
-
// Fit the map in view once its words are measured
|
|
275
|
+
// Fit the map in view once its words are measured, and again when a
|
|
276
|
+
// `value` puts another map in its place (above). The canvas fits only
|
|
238
277
|
// on its own first mount, before the topics are sized in the map's font
|
|
239
278
|
// (and again when web fonts land), so without this the map opened off to
|
|
240
279
|
// the top-left of an empty-looking canvas (2026-09-24).
|
|
@@ -243,10 +282,10 @@ export function MindMap({
|
|
|
243
282
|
// `value` in an effect of its own, and fits against what it holds.
|
|
244
283
|
let raf = 0;
|
|
245
284
|
const t = setTimeout(() => {
|
|
246
|
-
raf = requestAnimationFrame(() => (raf = requestAnimationFrame(() =>
|
|
285
|
+
raf = requestAnimationFrame(() => (raf = requestAnimationFrame(() => fitOpen({ duration: 0 }))));
|
|
247
286
|
}, 300);
|
|
248
287
|
return () => (clearTimeout(t), cancelAnimationFrame(raf));
|
|
249
|
-
}, [measure]);
|
|
288
|
+
}, [measure, refit, fitOpen]);
|
|
250
289
|
|
|
251
290
|
// The canvas pans with a plain drag (its hand tool): a mind map has
|
|
252
291
|
// nothing to lasso.
|
|
@@ -447,7 +486,7 @@ export function MindMap({
|
|
|
447
486
|
finishEdit();
|
|
448
487
|
commit(normalizeMap(map));
|
|
449
488
|
select(null);
|
|
450
|
-
setTimeout(() =>
|
|
489
|
+
setTimeout(() => fitOpen(), 80);
|
|
451
490
|
};
|
|
452
491
|
|
|
453
492
|
// ── announcements ────────────────────────────────────────────────────
|
|
@@ -901,7 +940,7 @@ export function MindMap({
|
|
|
901
940
|
emptyState={false}
|
|
902
941
|
templates={[]}
|
|
903
942
|
describeNode={describeTopic}
|
|
904
|
-
className=
|
|
943
|
+
className={String.raw`[&_.react-flow\_\_node]:transition-transform [&_.react-flow\_\_node]:duration-200 [&_.react-flow\_\_node]:ease-out motion-reduce:[&_.react-flow\_\_node]:transition-none`}
|
|
905
944
|
/>
|
|
906
945
|
</MindContext.Provider>
|
|
907
946
|
</div>
|
|
@@ -377,6 +377,39 @@ export const LEVELS = [
|
|
|
377
377
|
{ font: 14, weight: 400, line: 20, padX: 6, padY: 5, min: 44, max: 240, gap: 28 },
|
|
378
378
|
];
|
|
379
379
|
export const levelOf = (depth) => LEVELS[Math.min(depth, LEVELS.length - 1)];
|
|
380
|
+
|
|
381
|
+
/** The smallest zoom a map opens at: its smallest words (14px, a main
|
|
382
|
+
* branch's and a subtopic's) drawn no smaller than the kit's smallest
|
|
383
|
+
* text, text-2xs (11px). 11 / 14 ≈ 0.79. */
|
|
384
|
+
export const READABLE_ZOOM = 11 / Math.min(...LEVELS.map((l) => l.font));
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* How a map opens in a view `width` × `height` px, as options for the
|
|
388
|
+
* canvas's fitView: the whole map when it fits at READABLE_ZOOM or more
|
|
389
|
+
* (→ {}), else READABLE_ZOOM centred on the centre topic `rootId`. Until
|
|
390
|
+
* 0.5.1 a big map opened fitted whole whatever the zoom: 31% on a phone,
|
|
391
|
+
* its words unreadable. `nodes` are the board's ({ position, width,
|
|
392
|
+
* height }); `padding` is the fit's share of the view (the canvas's 0.2),
|
|
393
|
+
* split as React Flow splits it.
|
|
394
|
+
*/
|
|
395
|
+
export function openingView(nodes, { width, height, rootId, padding = 0.2 } = {}) {
|
|
396
|
+
if (!(width > 0 && height > 0) || !nodes?.length || rootId == null) return {};
|
|
397
|
+
let x1 = Infinity;
|
|
398
|
+
let y1 = Infinity;
|
|
399
|
+
let x2 = -Infinity;
|
|
400
|
+
let y2 = -Infinity;
|
|
401
|
+
for (const n of nodes) {
|
|
402
|
+
const x = n.position?.x ?? 0;
|
|
403
|
+
const y = n.position?.y ?? 0;
|
|
404
|
+
x1 = Math.min(x1, x);
|
|
405
|
+
y1 = Math.min(y1, y);
|
|
406
|
+
x2 = Math.max(x2, x + (n.width ?? 0));
|
|
407
|
+
y2 = Math.max(y2, y + (n.height ?? 0));
|
|
408
|
+
}
|
|
409
|
+
const room = (v) => v - 2 * Math.floor((v - v / (1 + padding)) / 2);
|
|
410
|
+
const fit = Math.min(room(width) / Math.max(1, x2 - x1), room(height) / Math.max(1, y2 - y1));
|
|
411
|
+
return fit >= READABLE_ZOOM ? {} : { nodes: [{ id: rootId }], minZoom: READABLE_ZOOM, maxZoom: READABLE_ZOOM };
|
|
412
|
+
}
|
|
380
413
|
const SIBLING_GAP = [0, 20, 8];
|
|
381
414
|
const siblingGap = (depth) => SIBLING_GAP[Math.min(depth, SIBLING_GAP.length - 1)];
|
|
382
415
|
/** Room left under a subtopic for its coloured line. */
|
|
@@ -485,7 +485,7 @@ export function OrgChart({
|
|
|
485
485
|
emptyState={false}
|
|
486
486
|
templates={[]}
|
|
487
487
|
describeNode={describePerson}
|
|
488
|
-
className=
|
|
488
|
+
className={String.raw`[&_.react-flow\_\_node]:transition-transform [&_.react-flow\_\_node]:duration-200 [&_.react-flow\_\_node]:ease-out motion-reduce:[&_.react-flow\_\_node]:transition-none`}
|
|
489
489
|
/>
|
|
490
490
|
</OrgContext.Provider>
|
|
491
491
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// viewer, a sheet on a narrow one (pdf-viewer.jsx decides). A click on an
|
|
6
6
|
// entry takes the pages to it; its note is edited in place.
|
|
7
7
|
import { useEffect, useRef, useState } from "react";
|
|
8
|
-
import { Button, Icon, Menu, Textarea, cx } from "../../ui";
|
|
8
|
+
import { Button, Icon, Menu, Textarea, cx, toLocalDate } from "../../ui";
|
|
9
9
|
import { sortHighlights } from "./helpers";
|
|
10
10
|
|
|
11
11
|
/** The colours a highlight can take, as token names (never a colour). */
|
|
@@ -188,8 +188,9 @@ function NoteItem({ h, active, editing, readOnly, onJump, onEdit, onSaveNote, on
|
|
|
188
188
|
);
|
|
189
189
|
}
|
|
190
190
|
|
|
191
|
+
// A day alone ("2026-09-28") is that day in the reader's zone, not UTC's.
|
|
191
192
|
function when(v) {
|
|
192
|
-
const d =
|
|
193
|
-
if (
|
|
193
|
+
const d = toLocalDate(v);
|
|
194
|
+
if (!d) return String(v);
|
|
194
195
|
return new Intl.DateTimeFormat(undefined, { day: "numeric", month: "short" }).format(d);
|
|
195
196
|
}
|
|
@@ -31,7 +31,7 @@ A note in `notes`:
|
|
|
31
31
|
id: "n-204",
|
|
32
32
|
author: { id: "u-lena", name: "Lena Fischer", src: "https://…/lena.jpg" },
|
|
33
33
|
body: [ /* the doc onAdd gave you (BlockNote JSON) */ ], // or a plain string
|
|
34
|
-
createdAt: "2026-09-23T09:12:00Z", // ISO string, Date or
|
|
34
|
+
createdAt: "2026-09-23T09:12:00Z", // ISO string, Date, ms, or a day ("2026-09-23": that day, never UTC's)
|
|
35
35
|
updatedAt: null, // set → "(edited)"
|
|
36
36
|
}
|
|
37
37
|
```
|
|
@@ -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>
|
package/src/shadcn.mjs
CHANGED
|
@@ -104,7 +104,7 @@ function coreItem() {
|
|
|
104
104
|
title: "Makersclaw kit",
|
|
105
105
|
description:
|
|
106
106
|
"The kit every Makersclaw block is built from — Button, Field, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction — " +
|
|
107
|
-
"and the parts bound to data (RecordSwitcher, useFixture). Native elements under the project's own theme tokens.",
|
|
107
|
+
"and the parts bound to data (RecordSwitcher, useFixture, LiveEmpty). Native elements under the project's own theme tokens.",
|
|
108
108
|
files,
|
|
109
109
|
cssVars: CSS_VARS,
|
|
110
110
|
css: CSS,
|