Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions .keikaku/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,14 +64,11 @@ in, the stored login is used and no token is needed.)

Composite library A–C done. Open threads, in rough priority order:

1. **Delete confirmation** in the library. Deletion is immediate and irreversible, matching
the old toolbar pills, but the library makes deleting far easier to reach. Needs a dialog
primitive - `components/ui/` has none, and `ModalOverlay` is now a reasonable base for one.
2. **Dragging nodes between parents** in the builder. Reordering currently stays within a
1. **Dragging nodes between parents** in the builder. Reordering currently stays within a
node's own sibling list.
3. **Wrapping existing nodes in a container**, so structure can be introduced after the fact
2. **Wrapping existing nodes in a container**, so structure can be introduced after the fact
instead of only planned up front.
4. Gen-UI: candidate follow-ups (from `docs/generative-ui.md` §6): quality evals + telemetry
3. Gen-UI: candidate follow-ups (from `docs/generative-ui.md` §6): quality evals + telemetry
on validation-failure rate (Phase 2), streaming preview via `streamObject`/SpecStream (Phase 3),
theming-aware generation (Phase 4). Smaller polish: wire an `AbortController`/cancel so a slow
generation can be cancelled instead of waiting out the 120s timeout (noted in review).
Expand Down Expand Up @@ -125,6 +122,15 @@ limitation. Tree manipulation moved into pure helpers in `lib/composite/tree.ts`
- **Notices are not validation errors.** A new `notice` type carries "N saved components
could not be loaded" without suppressing the "Valid" indicator.

**Found by dogfooding, then fixed (#30):** driving the app by hand rather than by test
exposed two destructive paths with no guard. Deleting a component that was placed on the
page silently broke the page ("Unknown component", one click, no confirmation) - inconsistent
with the elaborate warning built for _editing_ a component in use. And Escape, newly wired
when the overlays became real modals, discarded unsaved builder work in a single reflexive
keystroke - a regression introduced by the accessibility fix itself. Both now go through a
`ConfirmDialog` built on `ModalOverlay`; the delete prompt names how many sections it will
break, and the discard prompt only appears when the builder is actually dirty.

**Worth remembering:** a subagent dispatched **read-only** for review edited source anyway -
it injected `id: def.id` into `duplicateComposite` (which would make duplicating overwrite
the original) and left probe files behind. The test suite caught the mutation immediately,
Expand Down
29 changes: 29 additions & 0 deletions __tests__/sandbox/component-library.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,10 +158,39 @@ describe("ComponentLibrary", () => {
fireEvent.click(screen.getByTitle("Export Alpha Card"));
expect(props.onExportOne).toHaveBeenCalledWith(alpha);

// Delete is guarded: the card button only opens a confirmation.
fireEvent.click(screen.getByTitle("Delete Alpha Card"));
expect(props.onDelete).not.toHaveBeenCalled();
});

it("deletes only after the confirmation is accepted", () => {
const { props } = renderLibrary();

fireEvent.click(screen.getByTitle("Delete Alpha Card"));
expect(screen.getByText(/Delete "Alpha Card"\?/)).toBeDefined();

fireEvent.click(screen.getByRole("button", { name: "Delete" }));
expect(props.onDelete).toHaveBeenCalledWith("alpha");
});

it("does not delete when the confirmation is cancelled", () => {
const { props } = renderLibrary();

fireEvent.click(screen.getByTitle("Delete Alpha Card"));
fireEvent.click(screen.getByRole("button", { name: "Cancel" }));

expect(props.onDelete).not.toHaveBeenCalled();
expect(screen.queryByText(/Delete "Alpha Card"\?/)).toBeNull();
});

it("warns when the component is used by sections on the current page", () => {
renderLibrary({ usageById: { alpha: 2 } });

fireEvent.click(screen.getByTitle("Delete Alpha Card"));

expect(screen.getByText(/2 sections on the current page/)).toBeDefined();
});

it("fires the header actions", () => {
const { props } = renderLibrary();

Expand Down
10 changes: 10 additions & 0 deletions app/sandbox/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,15 @@ function SandboxContent() {
}, [parsedSections, parseFailed]);
const sectionsForUsage = parseFailed ? lastGoodSections.current : parsedSections;

// Per-component usage on the current page, so deleting one can say what it will break.
const compositeUsage = useMemo(() => {
const counts: Record<string, number> = {};
for (const section of sectionsForUsage) {
counts[section.component] = (counts[section.component] ?? 0) + 1;
}
return counts;
}, [sectionsForUsage]);

const knownCategories = useMemo(
() => [...new Set(composites.map((d) => d.category).filter((c): c is string => !!c))].sort(),
[composites],
Expand Down Expand Up @@ -739,6 +748,7 @@ function SandboxContent() {
onDelete={handleDeleteComposite}
onExportOne={handleExportOneComposite}
onExportAll={handleExportAllComposites}
usageById={compositeUsage}
onCreate={() => {
setLibraryOpen(false);
handleOpenBuilder();
Expand Down
Loading
Loading