diff options
| author | soryu <soryu@soryu.co> | 2026-05-16 18:04:17 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-05-16 18:04:17 +0100 |
| commit | 8e2bbcab1a7b3b9005803d7ce3bfce7fa483a4d7 (patch) | |
| tree | 9e21ac74362bae034c9749ab5d2815ddae84d8df /makima/frontend/src/routes/document-directives.tsx | |
| parent | dce7f50e503dc374aaf879df33e725af16c4cc78 (diff) | |
| download | soryu-8e2bbcab1a7b3b9005803d7ce3bfce7fa483a4d7.tar.gz soryu-8e2bbcab1a7b3b9005803d7ce3bfce7fa483a4d7.zip | |
fix(directives): tasks folder visibility, circular auto-save timer, unlock-to-edit (#133)
* feat: soryu - makima: Fix tasks/ folder visibility and rename for multi-contract directives
* feat: soryu - makima: Replace auto-save countdown text/bar with a circular timer
* feat: soryu - makima: Require Unlock before editing a locked contract body
Diffstat (limited to 'makima/frontend/src/routes/document-directives.tsx')
| -rw-r--r-- | makima/frontend/src/routes/document-directives.tsx | 54 |
1 files changed, 46 insertions, 8 deletions
diff --git a/makima/frontend/src/routes/document-directives.tsx b/makima/frontend/src/routes/document-directives.tsx index 06e427a..479dcd8 100644 --- a/makima/frontend/src/routes/document-directives.tsx +++ b/makima/frontend/src/routes/document-directives.tsx @@ -266,6 +266,11 @@ function DirectiveFolder({ return { activeDocs: active, shippedDocs: shipped }; }, [docs]); + // When a directive owns more than one contract, the two `tasks/` folders + // would otherwise be ambiguous. We pass this down to DocumentTasksFolder + // so it can rename itself to `tasks - <contract name>/` for clarity. + const multipleContracts = activeDocs.length + shippedDocs.length > 1; + const handleCreate = useCallback(async () => { if (creating) return; setCreating(true); @@ -433,6 +438,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + contractLabel={fileLabel(doc, directive)} + multipleContracts={multipleContracts} /> </div> ))} @@ -477,6 +484,8 @@ function DirectiveFolder({ refreshNonce={refreshNonce} selectedTaskId={selectedTaskIdForFolder} onSelectTask={onSelectTask} + contractLabel={fileLabel(doc, directive)} + multipleContracts={multipleContracts} /> </div> ))} @@ -627,6 +636,13 @@ interface DocumentTasksFolderProps { selectedTaskId: string | null; /** Click handler for step/task rows — navigates to the live transcript. */ onSelectTask: (directiveId: string, taskId: string) => void; + /** Human-readable contract label (already resolved via fileLabel). Used to + * disambiguate multiple tasks/ folders under the same directive. */ + contractLabel: string; + /** True when the parent directive owns more than one contract — drives the + * `tasks - <contractLabel>/` rename so the two sibling tasks/ folders are + * distinguishable. Single-contract directives keep the plain `tasks/`. */ + multipleContracts: boolean; } function DocumentTasksFolder({ @@ -637,6 +653,8 @@ function DocumentTasksFolder({ refreshNonce, selectedTaskId, onSelectTask, + contractLabel, + multipleContracts, }: DocumentTasksFolderProps) { const [open, setOpen] = useState(defaultOpen); const [data, setData] = useState<ContractTasksResponse | null>(null); @@ -672,24 +690,29 @@ function DocumentTasksFolder({ const total = (data?.steps.length ?? 0) + (data?.tasks.length ?? 0); - // Don't render the folder at all if we've fetched and the document has - // no tasks. This is the cleanest visual: a draft document just shows up - // as a single row with no children. The empty-folder check is gated on - // a successful fetch so we don't flash "no tasks/" rows during loading. - if (data && total === 0 && !loading && !error) { - return null; - } + // Folder always renders (even when empty) so the user can click into a + // fresh contract's tasks/ folder and see it stay visible. The empty state + // shows a muted "no tasks yet" placeholder inside the open body — same + // visual weight as the existing "Loading tasks…" / error placeholders. + // + // When the parent directive owns multiple contracts, both tasks/ folders + // are disambiguated as `tasks - <contractLabel>/` so the user can tell + // them apart. Single-contract directives keep the plain `tasks/` label. + const headerLabel = multipleContracts + ? `tasks - ${contractLabel}/` + : "tasks/"; return ( <div> <button type="button" onClick={() => setOpen((v) => !v)} + title={headerLabel} className={`w-full flex items-center gap-1.5 ${headerPadLeft} pr-3 py-1 font-mono text-[11px] text-[#7788aa] hover:bg-[rgba(117,170,252,0.06)]`} > <Caret open={open} /> <FolderIcon open={open} /> - <span>tasks/</span> + <span className="truncate text-left">{headerLabel}</span> {total > 0 && ( <span className="ml-auto text-[10px] text-[#556677]">{total}</span> )} @@ -706,6 +729,11 @@ function DocumentTasksFolder({ {error} </div> )} + {data && total === 0 && !loading && !error && ( + <div className={`${rowPadLeft} pr-3 py-1 font-mono text-[10px] text-[#556677] italic`}> + no tasks yet + </div> + )} {data?.steps.map((step) => ( <StepRow key={`step-${step.id}`} @@ -1428,6 +1456,16 @@ function EditorShell({ onPickUpOrders={async () => { await pickUpOrders(); }} + // Locked-and-started (`active`), `queued`, `shipped`, and + // `archived` contracts must be unlocked before edits are + // accepted. Only `draft` is freely editable; everything else + // shows the in-editor Unlock affordance. + editable={doc.status === "draft"} + onRequestUnlock={async () => { + const updated = await unlockDirectiveContract(doc.id); + setDoc(updated); + onDocumentChanged(); + }} /> </div> ); |
