makersclaw 0.8.2 → 0.8.4
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/activity-log.json +3 -3
- package/r/agent-changes.json +3 -3
- package/r/agent-chat.json +3 -3
- package/r/agent-column.json +3 -3
- package/r/agent-dock.json +3 -3
- package/r/agent-kit.json +2 -2
- package/r/agent-marks.json +2 -2
- package/r/agent-pane.json +6 -6
- package/r/ai-action.json +4 -4
- package/r/annotate.json +3 -3
- package/r/approval-request.json +2 -2
- package/r/approval.json +3 -3
- package/r/ask-bar.json +2 -2
- package/r/audio-player.json +2 -2
- package/r/auth-form.json +2 -2
- package/r/automation-editor.json +7 -7
- package/r/automation-list.json +3 -3
- package/r/automation-model.json +2 -2
- package/r/automations-screen.json +7 -7
- package/r/board.json +2 -2
- package/r/calendar-view.json +3 -3
- package/r/cart.json +2 -2
- package/r/chart.json +2 -2
- package/r/chat-pane.json +4 -4
- package/r/chat.json +2 -2
- package/r/code-editor.json +2 -2
- package/r/comment-box.json +3 -3
- package/r/connect-card.json +2 -2
- package/r/contact-section.json +2 -2
- package/r/cost.json +2 -2
- package/r/csv-import.json +3 -3
- package/r/data-picker.json +3 -3
- package/r/data-table.json +2 -2
- 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/first-run.json +2 -2
- package/r/fix-note.json +4 -4
- package/r/flow-canvas.json +2 -2
- package/r/flow-editor.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/library-shelf.json +2 -2
- package/r/map.json +2 -2
- package/r/margin-notes.json +3 -3
- package/r/menubar.json +2 -2
- package/r/mind-map.json +3 -3
- package/r/navigation-menu.json +2 -2
- package/r/needs-you.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +3 -3
- package/r/pace-bar.json +3 -3
- 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 +2 -2
- package/r/product-detail.json +2 -2
- package/r/product-grid.json +3 -3
- package/r/record-form.json +2 -2
- package/r/record-grid.json +2 -2
- package/r/record-notes.json +4 -4
- package/r/registry.json +249 -249
- package/r/run-history.json +2 -2
- package/r/run-view.json +6 -6
- package/r/schedule-card.json +2 -2
- package/r/schedule-engine.json +2 -2
- package/r/schedule-gantt.json +6 -6
- package/r/schedule-ruler.json +2 -2
- 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 +2 -2
- package/r/signature-pad.json +2 -2
- package/r/sketch-canvas.json +2 -2
- package/r/source-receipt.json +3 -3
- package/r/spreadsheet.json +2 -2
- package/r/stat-cards.json +3 -3
- package/r/step-picker.json +3 -3
- package/r/step-settings.json +5 -5
- package/r/suggestion.json +2 -2
- package/r/sum-bar.json +2 -2
- package/r/task-list.json +3 -3
- package/r/task-model.json +2 -2
- package/r/testimonials.json +2 -2
- package/r/tier-map.json +2 -2
- package/r/time-grid.json +2 -2
- package/r/time-strip.json +5 -5
- package/r/top-bar.json +2 -2
- package/r/transaction-list.json +2 -2
- package/r/trigger-picker.json +3 -3
- package/r/whiteboard.json +3 -3
- package/r/workflow-view.json +3 -3
- package/registry/blocks/agent-pane/README.md +7 -0
- package/registry/blocks/agent-pane/agent-pane.jsx +23 -18
- package/registry/blocks/chat-pane/README.md +4 -0
- package/registry/blocks/chat-pane/chat-attach.js +6 -1
- package/registry/blocks/schedule-gantt/schedule-gantt.jsx +15 -10
- package/registry/blocks/schedule-gantt/sheet.jsx +8 -6
- package/registry/blocks/schedule-gantt/track.jsx +1 -1
- package/registry/blocks/time-strip/README.md +5 -2
- package/registry/blocks/time-strip/strip-model.js +6 -0
- package/registry/blocks/time-strip/time-strip.jsx +41 -4
package/r/trigger-picker.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "TriggerPicker",
|
|
6
6
|
"description": "What starts an automation, as a panel: by hand, on a schedule in the team's time zone, when an app sends an event, when one of an app's actions finishes, when a record is added or changed, when the automation's own web address is called (its secret made and replaced here, shown once), or when another automation finishes. The platform says which of these it runs (the list door's triggers); a connection's own events are listed as \"soon\" with what to use meanwhile. The schedule is offered in the shapes people ask for (every day, every weekday, every week, every hour, another schedule as five-field cron) with a time of day, a weekday, the team's zone or another, and a sentence saying it back (\"Runs weekdays at 07:00, team time\").",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/automation-model.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"docs": "Installed to components/makersclaw/blocks/trigger-picker. Import: import { TriggerPicker } from \"@/components/makersclaw/blocks/trigger-picker\". Props and an example: components/makersclaw/blocks/trigger-picker/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
55
55
|
"meta": {
|
|
56
56
|
"makersclaw": {
|
|
57
|
-
"version": "0.8.
|
|
57
|
+
"version": "0.8.4",
|
|
58
58
|
"useWhen": "An automation's trigger is edited in your own screen. automation-editor opens it when the trigger card is pressed; pass it the platform's triggers (useAutomations().triggers) so it offers what the platform runs."
|
|
59
59
|
}
|
|
60
60
|
}
|
package/r/whiteboard.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "Whiteboard",
|
|
6
6
|
"description": "A full-page freeform whiteboard on sketch-canvas: a top bar with the board's title (click to rename), the save state (Saving…, Saved, or the error with Retry), a Board / Text view switch, Export PNG and an optional Share button; under it the Miro-style canvas (tool rail, floating toolbar, zoom, undo). An empty board offers starter templates — blank, a grid of titled areas, a 2×2 matrix — and the Text view lists every word on the board for a screen reader or an agent. With onCreateTask, a selected note or text becomes a task from the top bar (\"Create task\", editable title; several notes make one task each), and a small badge marks it on the board from then on. The sketch is plain JSON; saves are async callbacks answering { ok }.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/sketch-canvas.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"docs": "Installed to components/makersclaw/blocks/whiteboard. Import: import { Whiteboard, WHITEBOARD_TEMPLATES, gridSketch, matrixSketch } from \"@/components/makersclaw/blocks/whiteboard\". Props and an example: components/makersclaw/blocks/whiteboard/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
60
60
|
"meta": {
|
|
61
61
|
"makersclaw": {
|
|
62
|
-
"version": "0.8.
|
|
62
|
+
"version": "0.8.4",
|
|
63
63
|
"useWhen": "A screen is one board to think on: a team whiteboard, a planning or prioritisation board, a place to sketch an idea and keep it with a record. Not for marking up a screenshot (annotate), sketching a UI to build (sketch-to-app), a structured board of cards and connectors (flow-canvas), or a kanban (board)."
|
|
64
64
|
}
|
|
65
65
|
}
|
package/r/workflow-view.json
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
"title": "WorkflowView",
|
|
6
6
|
"description": "One job of the app's agent drawn as a graph on a dotted ground: the Trigger (\"Every weekday at 07:00\", \"When a deal is created\") → each step of the chosen run with how it went, as a mark and a word → the result. Over it a run picker with Re-run; in its head the Live switch and \"Trigger manually\" with its price; beside it the run history and the overview (run-history). Before any run it draws the job's declared steps in pencil, with an honest empty line. Fed by useJobs, useRuns and useRun (lib/hooks.js). Under about 768 px of its own width the history stacks under the graph and the graph runs top to bottom.",
|
|
7
7
|
"registryDependencies": [
|
|
8
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
9
|
-
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.
|
|
8
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/core.json",
|
|
9
|
+
"https://cdn.jsdelivr.net/npm/makersclaw@0.8.4/r/run-history.json"
|
|
10
10
|
],
|
|
11
11
|
"dependencies": [],
|
|
12
12
|
"files": [
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"docs": "Installed to components/makersclaw/blocks/workflow-view. Import: import { WorkflowView } from \"@/components/makersclaw/blocks/workflow-view\". Props and an example: components/makersclaw/blocks/workflow-view/README.md. Makersclaw builds whole apps from these blocks: https://makersclaw.com",
|
|
49
49
|
"meta": {
|
|
50
50
|
"makersclaw": {
|
|
51
|
-
"version": "0.8.
|
|
51
|
+
"version": "0.8.4",
|
|
52
52
|
"useWhen": "Superseded by automation-editor and run-view for an app whose jobs run on the automations engine; still the block for an app that has not moved (its jobs are the agent's wakes). A job's own page: the founder opens an automation to see what it does, how its last runs went and to run it now — a morning read, an inbox sweep, a nightly enrichment, an event-triggered workflow. For the job as a card beside the day's work use schedule-card; for a plain list of runs use run-history."
|
|
53
53
|
}
|
|
54
54
|
}
|
|
@@ -64,6 +64,13 @@ Only a plan or a piece of one, a change the agent sent up, a counted job
|
|
|
64
64
|
and a hand-off that never started get a new request; for those
|
|
65
65
|
`chat.sameConversation` is false and the box keeps the app's own words.
|
|
66
66
|
|
|
67
|
+
## Progress feedback
|
|
68
|
+
|
|
69
|
+
The transcript owns progress feedback: one “Thinking…” line while no live
|
|
70
|
+
text or specific tool work is moving, or an explicit queued line while the
|
|
71
|
+
ask waits. A pending follow-up is never labelled “Starting…”. The composer
|
|
72
|
+
keeps Stop available while the turn runs; there is no second working footer.
|
|
73
|
+
|
|
67
74
|
## Questions keep the conversation open
|
|
68
75
|
|
|
69
76
|
While a question waits, the main composer stays available. Sending there
|
|
@@ -38,10 +38,20 @@ import { MemberWords, Message } from "./transcript";
|
|
|
38
38
|
// draws an Approve of its own. A sign-in, money, or a tool approval says
|
|
39
39
|
// so and opens the desk.
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
/** Specific work or streamed words already tell the member what is moving. */
|
|
42
|
+
function showsProgress(message) {
|
|
43
|
+
if (message.role !== "assistant" || !message.live) return false;
|
|
44
|
+
return (message.blocks ?? []).some((block) => {
|
|
45
|
+
if (block.kind === "text") return block.streaming && Boolean(block.text?.trim());
|
|
46
|
+
if (block.kind === "plan") return block.items?.some((item) => item.state === "active");
|
|
47
|
+
if (block.kind !== "work") return false;
|
|
48
|
+
return (block.runs ?? []).some((run) =>
|
|
49
|
+
run.kind === "fold" ? run.summary?.running :
|
|
50
|
+
run.kind === "helpers" ? run.calls?.some((call) => call.status === "running") :
|
|
51
|
+
run.kind === "thought" ? run.streaming : run.status === "running",
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
45
55
|
|
|
46
56
|
/** What waits on the member, as the bar under the thread says it. */
|
|
47
57
|
function needsTitle(kind, inApp) {
|
|
@@ -123,12 +133,13 @@ export function AgentPane({
|
|
|
123
133
|
const inMakersclaw = chat?.place === "makersclaw";
|
|
124
134
|
const questions = view?.questions ?? [];
|
|
125
135
|
const result = chat?.request?.status === "done" ? chat.request?.result?.sentence : null;
|
|
126
|
-
//
|
|
127
|
-
//
|
|
136
|
+
// One generic progress row, only while nothing specific visibly moves.
|
|
137
|
+
// Pending words are a send, not proof that a worker is starting.
|
|
128
138
|
const lastLegacy = legacy[legacy.length - 1];
|
|
129
|
-
const
|
|
139
|
+
const visibleProgress = !pending && (view ? messages.some(showsProgress) : Boolean(lastLegacy?.writing || lastLegacy?.parts?.some((part) => part.type === "step" && part.status === "running")));
|
|
140
|
+
const queued = chat?.state === "queued";
|
|
130
141
|
const thinking =
|
|
131
|
-
!needs && !inMakersclaw && (
|
|
142
|
+
live && !needs && !inMakersclaw && (queued || !visibleProgress);
|
|
132
143
|
|
|
133
144
|
const openThing = onOpen ?? (typeof chat?.open === "function" ? chat.open : undefined);
|
|
134
145
|
const answerHere = needs?.in_app === "answer" && typeof chat?.answer === "function";
|
|
@@ -139,8 +150,7 @@ export function AgentPane({
|
|
|
139
150
|
const closedWords = needs ? (answerHere ? "Answer above first" : pressHere ? "Say yes above first" : "Answer on the desk first") : "This ask is in Makersclaw";
|
|
140
151
|
|
|
141
152
|
// What sits between the lines and the box: what waits on the member, or
|
|
142
|
-
//
|
|
143
|
-
// how the ask ended.
|
|
153
|
+
// how the ask ended. Progress is in the transcript; Stop is the box's.
|
|
144
154
|
const bar =
|
|
145
155
|
needs && pressHere ? (
|
|
146
156
|
<div className="max-h-[70%] shrink-0 overflow-y-auto border-t border-border p-2.5">{yes}</div>
|
|
@@ -169,12 +179,7 @@ export function AgentPane({
|
|
|
169
179
|
)}
|
|
170
180
|
</div>
|
|
171
181
|
</div>
|
|
172
|
-
) : live &&
|
|
173
|
-
<div className="flex shrink-0 items-center gap-2 border-t border-border px-3.5 py-1.5 text-xs text-muted-foreground">
|
|
174
|
-
<Spinner size="sm" label="Working" />
|
|
175
|
-
<span className="min-w-0 flex-1 truncate">{STATE_LINE[chat?.state] ?? STATE_LINE.working}</span>
|
|
176
|
-
</div>
|
|
177
|
-
) : result ? (
|
|
182
|
+
) : !live && result ? (
|
|
178
183
|
<p className="flex shrink-0 items-start gap-2 border-t border-border px-3.5 py-1.5 text-xs text-muted-foreground">
|
|
179
184
|
<Icon name="circle-check" size={14} className="mt-px shrink-0 text-success" />
|
|
180
185
|
<span className="min-w-0 break-words">{result}</span>
|
|
@@ -222,8 +227,8 @@ export function AgentPane({
|
|
|
222
227
|
))}
|
|
223
228
|
{pending ? <MemberWords>{pending}</MemberWords> : null}
|
|
224
229
|
{thinking ? (
|
|
225
|
-
<div role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
226
|
-
<Spinner size="sm" label="Thinking" /> {
|
|
230
|
+
<div role="status" aria-label="Agent progress" className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
231
|
+
<Spinner size="sm" label={queued ? "Queued" : "Thinking"} /> {queued ? "Queued — it starts when the one before it ends" : "Thinking…"}
|
|
227
232
|
</div>
|
|
228
233
|
) : null}
|
|
229
234
|
{!live && view?.failure ? (
|
|
@@ -141,3 +141,7 @@ export default function Notes() {
|
|
|
141
141
|
);
|
|
142
142
|
}
|
|
143
143
|
```
|
|
144
|
+
|
|
145
|
+
Dictation capability is detected after hydration: the server and first client
|
|
146
|
+
render show the same composer, then supported browsers gain the microphone.
|
|
147
|
+
This detection itself does not ask for microphone access.
|
|
@@ -86,7 +86,12 @@ function recognitionCtor() {
|
|
|
86
86
|
* refused (a frame the desk has not allowed it in, or the member said no).
|
|
87
87
|
*/
|
|
88
88
|
export function useDictation(onText) {
|
|
89
|
-
|
|
89
|
+
// Server and first client render must draw the same composer. Browser
|
|
90
|
+
// capability becomes available only after hydration (no microphone is used).
|
|
91
|
+
const [supported, setSupported] = useState(false);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
setSupported(recognitionCtor() !== null);
|
|
94
|
+
}, []);
|
|
90
95
|
const [listening, setListening] = useState(false);
|
|
91
96
|
const [interim, setInterim] = useState("");
|
|
92
97
|
const [error, setError] = useState(null);
|
|
@@ -321,7 +321,9 @@ export function ScheduleGantt({
|
|
|
321
321
|
const together = modeName === "move" && picked.size > 1 && picked.has(String(id)) ? [...picked].map(String) : null;
|
|
322
322
|
dragRef.current = { id: String(id), mode: modeName, x0: e.clientX, start: shown.get(String(id))?.start, delta: 0, moved: false, pointerId: e.pointerId, together };
|
|
323
323
|
try {
|
|
324
|
-
track
|
|
324
|
+
// Keep an ordinary click/double-click on its bar; capture on the track
|
|
325
|
+
// retargeted that click and skipped the bar's selection/open callbacks.
|
|
326
|
+
e.currentTarget?.setPointerCapture(e.pointerId);
|
|
325
327
|
} catch {
|
|
326
328
|
// the pointer is gone already
|
|
327
329
|
}
|
|
@@ -781,6 +783,7 @@ export function ScheduleGantt({
|
|
|
781
783
|
</div>
|
|
782
784
|
);
|
|
783
785
|
}
|
|
786
|
+
const shortBar = w <= 16;
|
|
784
787
|
const loop = r.stuck;
|
|
785
788
|
const guess = guesses?.has?.(e.id);
|
|
786
789
|
const barTop = y - BAR / 2 - (baseline ? 2 : 0);
|
|
@@ -797,7 +800,7 @@ export function ScheduleGantt({
|
|
|
797
800
|
{...common}
|
|
798
801
|
onPointerDown={(ev) => startDrag(ev, e.id, "move")}
|
|
799
802
|
className={cx(
|
|
800
|
-
"absolute inset-0
|
|
803
|
+
"absolute inset-0 rounded-[2px] outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
801
804
|
trackClass,
|
|
802
805
|
isSel && "ring-2 ring-ring",
|
|
803
806
|
(dragging || movedByDrag) && !isSel && "ring-[1.5px] ring-ring",
|
|
@@ -806,16 +809,18 @@ export function ScheduleGantt({
|
|
|
806
809
|
onMove && !done && "cursor-grab",
|
|
807
810
|
)}
|
|
808
811
|
>
|
|
809
|
-
|
|
810
|
-
|
|
812
|
+
<span aria-hidden="true" className="pointer-events-none absolute inset-0 overflow-hidden rounded-[2px]">
|
|
813
|
+
{loop ? <span className="absolute inset-0 opacity-40" style={{ backgroundImage: "repeating-linear-gradient(135deg, currentColor 0 2px, transparent 2px 6px)" }} /> : null}
|
|
814
|
+
{!loop && !guess && !done && r.progress > 0 ? <span className={cx("absolute inset-y-0 left-0", fill)} style={{ width: `${r.progress}%` }} /> : null}
|
|
815
|
+
</span>
|
|
816
|
+
{/* A press on an end is still a press on this task. With no
|
|
817
|
+
drag, its click/open callbacks must survive clipping. */}
|
|
818
|
+
{onMove && !done ? <>
|
|
819
|
+
<span onPointerDown={(ev) => startDrag(ev, e.id, "start")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", shortBar ? (dragging || isSel ? "-inset-y-[2px] -left-1.5 w-1.5 rounded-[2px] bg-ring" : "inset-y-0 -left-1.5 w-1.5") : dragging || isSel ? "-inset-y-[2px] -left-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 left-0 w-2")} style={{ touchAction: "none" }} />
|
|
820
|
+
<span onPointerDown={(ev) => startDrag(ev, e.id, "end")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", shortBar ? (dragging || isSel ? "-inset-y-[2px] -right-1.5 w-1.5 rounded-[2px] bg-ring" : "inset-y-0 -right-1.5 w-1.5") : dragging || isSel ? "-inset-y-[2px] -right-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 right-0 w-2")} style={{ touchAction: "none" }} />
|
|
821
|
+
</> : null}
|
|
811
822
|
</button>
|
|
812
823
|
{base ? <span aria-hidden="true" className="pointer-events-none absolute h-1 rounded-[1px] bg-chart-3" style={{ left: X(base.start) - x0, width: Math.max(ppd, X(base.due) + ppd - X(base.start)), top: BAR + 2 }} /> : null}
|
|
813
|
-
{onMove && !done ? (
|
|
814
|
-
<>
|
|
815
|
-
<span onPointerDown={(ev) => startDrag(ev, e.id, "start")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", dragging || isSel ? "-inset-y-[2px] -left-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 left-0 w-2")} style={{ touchAction: "none" }} />
|
|
816
|
-
<span onPointerDown={(ev) => startDrag(ev, e.id, "end")} aria-hidden="true" className={cx("absolute z-10 cursor-ew-resize", dragging || isSel ? "-inset-y-[2px] -right-[2px] w-1.5 rounded-[2px] bg-ring" : "inset-y-0 right-0 w-2")} style={{ touchAction: "none" }} />
|
|
817
|
-
</>
|
|
818
|
-
) : null}
|
|
819
824
|
{handles}
|
|
820
825
|
{labelEl(w, isSel ? 8 : 0)}
|
|
821
826
|
</div>
|
|
@@ -313,12 +313,14 @@ export function SheetRow({ e, t, r, cols, selected, closed, cells, tags, late, e
|
|
|
313
313
|
export function AddRow({ open, onOpen, draft, setDraft, onSubmit, busy, addRef, parentTitle, aboveTitle, onIndent, height = ROW }) {
|
|
314
314
|
if (!open)
|
|
315
315
|
return (
|
|
316
|
-
<button type="button" onClick={onOpen} className="flex w-full items-center gap-2 border-b border-border px-3 text-left text-[13px] text-muted-foreground outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" style={{ height }}>
|
|
317
|
-
<Icon name="plus" size={14} />
|
|
318
|
-
<span>Add task</span>
|
|
319
|
-
<kbd className="rounded-[3px] border border-border px-1 text-[10px]">Enter</kbd>
|
|
320
|
-
<span>
|
|
321
|
-
|
|
316
|
+
<button type="button" onClick={onOpen} title="Enter adds a task; Shift+Enter adds a milestone." aria-keyshortcuts="Enter Shift+Enter" className="@container/gantt-add flex w-full min-w-0 items-center gap-2 border-b border-border px-3 text-left text-[13px] text-muted-foreground outline-none hover:bg-muted/50 focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" style={{ height }}>
|
|
317
|
+
<Icon name="plus" size={14} className="shrink-0" />
|
|
318
|
+
<span className="min-w-0 flex-1 truncate">Add task</span>
|
|
319
|
+
<kbd className="shrink-0 rounded-[3px] border border-border px-1 text-[10px]">Enter</kbd>
|
|
320
|
+
<span className="hidden shrink-0 items-center gap-2 @[18rem]/gantt-add:flex">
|
|
321
|
+
<span>milestone</span>
|
|
322
|
+
<kbd className="rounded-[3px] border border-border px-1 text-[10px]">Shift+Enter</kbd>
|
|
323
|
+
</span>
|
|
322
324
|
</button>
|
|
323
325
|
);
|
|
324
326
|
return (
|
|
@@ -26,7 +26,7 @@ export function TrackHead({ axis, range, ppd, z, gridW, marks }) {
|
|
|
26
26
|
<>
|
|
27
27
|
{axis.top.map((s) => (
|
|
28
28
|
<div key={`t${s.start}`} className="absolute top-0 h-[18px] border-l border-border" style={{ left: (s.start - range.from) * ppd, width: (s.end - s.start + 1) * ppd }}>
|
|
29
|
-
<span className="sticky block w-max px-1.5 pt-0.5 text-[11px] font-semibold text-foreground" style={{ left: gridW }}>
|
|
29
|
+
<span title={s.label} className="sticky block w-max max-w-full truncate px-1.5 pt-0.5 text-[11px] font-semibold text-foreground" style={{ left: gridW }}>
|
|
30
30
|
{s.label}
|
|
31
31
|
</span>
|
|
32
32
|
</div>
|
|
@@ -25,7 +25,7 @@ import { TimeStrip } from "@/components/makersclaw/blocks/time-strip"; // a sh
|
|
|
25
25
|
|
|
26
26
|
A client component. No other block and no npm package. Positions are
|
|
27
27
|
percentages of the length, so the server and the browser draw the same picture;
|
|
28
|
-
|
|
28
|
+
the rendered width is measured to keep crowded mark controls separate.
|
|
29
29
|
|
|
30
30
|
## Times
|
|
31
31
|
|
|
@@ -58,7 +58,10 @@ is `"04:12"`.
|
|
|
58
58
|
- `selected` — the id of the mark to show pressed (taller, ringed).
|
|
59
59
|
- `onMark(mark)` — makes the marks buttons. Called with the mark as laid out
|
|
60
60
|
(`{ ...yours, seconds, share, pct }`). ← → on a focused mark moves to the next
|
|
61
|
-
or the one before.
|
|
61
|
+
or the one before. When their hit regions would overlap, the exact visual
|
|
62
|
+
marks remain on the track and a scrolling row of 44px timestamp buttons
|
|
63
|
+
selects each mark separately. It returns to direct track buttons when there
|
|
64
|
+
is room. Without `onMark` the marks are plain pictures with a label.
|
|
62
65
|
- `size` — `sm` (22 px, default) or `lg` (44 px).
|
|
63
66
|
- `axis` — `true` (default): about `ticks` labels under the bar on whole steps,
|
|
64
67
|
always the start and the end. `false`: none. Or your own labels,
|
|
@@ -176,3 +176,9 @@ export function stripWords({ length, marks, noun = "mark" }) {
|
|
|
176
176
|
const word = n === 1 ? noun : `${noun}s`;
|
|
177
177
|
return n ? `${n} ${word} in ${clockOf(total)}` : `No ${noun}s in ${clockOf(total)}`;
|
|
178
178
|
}
|
|
179
|
+
|
|
180
|
+
/** Whether neighboring hit regions would overlap at the rendered track width. */
|
|
181
|
+
export function marksAreDense(placed, width, hit = 20) {
|
|
182
|
+
if (!(width > 0)) return false;
|
|
183
|
+
return placed.some((mark, index) => index > 0 && (mark.share - placed[index - 1].share) * width < hit);
|
|
184
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useRef, useState } from "react";
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
4
|
import { Button, Notice, Skeleton, cx } from "../../ui";
|
|
5
|
-
import { clockOf, countMarks, lengthOf, marksOf, nextMark, pctOf, rangeOf, rangesOf, secondsAt, shareOf, stripWords, ticksFor, toSeconds } from "./strip-model";
|
|
5
|
+
import { clockOf, countMarks, lengthOf, marksAreDense, marksOf, nextMark, pctOf, rangeOf, rangesOf, secondsAt, shareOf, stripWords, ticksFor, toSeconds } from "./strip-model";
|
|
6
6
|
|
|
7
7
|
// One bar for a span that has a length (a call, a recording, a shift), with
|
|
8
8
|
// marks at the times that matter on it. The marks are the app's: a cut, a
|
|
@@ -63,9 +63,20 @@ export function TimeStrip({
|
|
|
63
63
|
}) {
|
|
64
64
|
const track = useRef(null);
|
|
65
65
|
const [drag, setDrag] = useState(null);
|
|
66
|
+
const [trackWidth, setTrackWidth] = useState(0);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const element = track.current;
|
|
69
|
+
if (!element) return;
|
|
70
|
+
const measure = () => setTrackWidth(element.getBoundingClientRect().width);
|
|
71
|
+
measure();
|
|
72
|
+
const observer = new ResizeObserver(measure);
|
|
73
|
+
observer.observe(element);
|
|
74
|
+
return () => observer.disconnect();
|
|
75
|
+
}, [status]);
|
|
66
76
|
const total = lengthOf(length);
|
|
67
77
|
const look = SIZE[size] ?? SIZE.sm;
|
|
68
78
|
const placed = marksOf(marks, total);
|
|
79
|
+
const dense = Boolean(onMark) && marksAreDense(placed, trackWidth);
|
|
69
80
|
const gaps = rangesOf(away, total);
|
|
70
81
|
const shown = drag ?? (selection ? rangeOf(selection.from, selection.to, total) : null);
|
|
71
82
|
const canPick = Boolean(onSelect) && total > 0;
|
|
@@ -189,7 +200,7 @@ export function TimeStrip({
|
|
|
189
200
|
on && "ring-2 ring-background",
|
|
190
201
|
);
|
|
191
202
|
const style = { "--at": `${m.pct}%` };
|
|
192
|
-
return onMark ? (
|
|
203
|
+
return onMark && !dense ? (
|
|
193
204
|
<button
|
|
194
205
|
key={m.id}
|
|
195
206
|
type="button"
|
|
@@ -212,7 +223,7 @@ export function TimeStrip({
|
|
|
212
223
|
data-mark={m.id}
|
|
213
224
|
/>
|
|
214
225
|
) : (
|
|
215
|
-
<span key={m.id} role="img" aria-label={name} title={name} className={className} style={style} />
|
|
226
|
+
<span key={m.id} role="img" aria-label={name} title={name} className={cx(className, dense && "pointer-events-none")} style={style} />
|
|
216
227
|
);
|
|
217
228
|
})}
|
|
218
229
|
{canPick && shown && shown.widthPct > 0
|
|
@@ -250,6 +261,32 @@ export function TimeStrip({
|
|
|
250
261
|
))}
|
|
251
262
|
</div>
|
|
252
263
|
) : null}
|
|
264
|
+
{dense ? (
|
|
265
|
+
<div className="grid min-w-0 gap-1">
|
|
266
|
+
<p className="text-xs text-muted-foreground">Marks are close together. Choose a time:</p>
|
|
267
|
+
<div role="group" aria-label={`${label} marks`} className="flex min-w-0 gap-1 overflow-x-auto pb-1">
|
|
268
|
+
{placed.map((mark) => (
|
|
269
|
+
<Button
|
|
270
|
+
key={mark.id}
|
|
271
|
+
data-mark={mark.id}
|
|
272
|
+
variant={selected === mark.id ? "secondary" : "quiet"}
|
|
273
|
+
aria-label={`${mark.label ? `${mark.label}, ` : ""}${noun} at ${clockOf(mark.seconds)}`}
|
|
274
|
+
aria-pressed={selected === mark.id}
|
|
275
|
+
title={mark.label}
|
|
276
|
+
className="shrink-0 tabular-nums"
|
|
277
|
+
style={{ minWidth: 44, minHeight: 44 }}
|
|
278
|
+
onClick={() => onMark(mark)}
|
|
279
|
+
onKeyDown={(event) => {
|
|
280
|
+
if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return;
|
|
281
|
+
const to = nextMark(placed, mark.id, event.key === "ArrowRight" ? 1 : -1);
|
|
282
|
+
const element = event.currentTarget.parentElement?.querySelector(`[data-mark="${CSS.escape(String(to))}"]`);
|
|
283
|
+
if (element && to !== mark.id) { event.preventDefault(); element.focus(); }
|
|
284
|
+
}}
|
|
285
|
+
>{clockOf(mark.seconds)}</Button>
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
) : null}
|
|
253
290
|
<span className="sr-only">
|
|
254
291
|
{words}
|
|
255
292
|
{Object.keys(kinds).length > 1 ? `, in ${Object.keys(kinds).length} kinds` : ""}
|