Fix dataset-ref walk false positive and wire spec-mandated success toasts

- Prune the data payload and top-level datasets keys in all three ref walks
  (extractDatasetRefs, renameDatasetInSpec, resolveDatasetRefs) so a data row
  carrying a field named "data" is no longer misread as a library reference,
  spuriously rewritten, or made to throw DatasetNotFoundError. Adds tests,
  including a guard that lookup-transform refs (from.data) still resolve.
- Wire the deferred success toasts now the Toaster has landed: publish, revert,
  extract-to-dataset, and snippet/dataset delete. Copy follows the council
  title-vs-message rule (title states the action, message adds the consequence).
- Reconcile the spec's blanket toast mandate to "toast only what the user can't
  already see": no toast on visible-result creates (snippet, dataset form);
  Copy Reference stays inline and gains an aria-live announcement (new shared
  .visually-hidden utility) instead of a toast-per-copy.
- Move the toast region to bottom-right so it stops covering the header action
  cluster (Publish/Revert, theme/datasets).
- Update docs/spec 01F/02/05 and docs/architecture/07 + 10 to match.
This commit is contained in:
2026-06-05 16:34:58 +03:00
parent a4e4d96d3b
commit 693f5d7073
17 changed files with 263 additions and 67 deletions
+17 -2
View File
@@ -191,8 +191,16 @@ function DatasetDetail({
confirmLabel: 'Delete',
danger: true,
});
// TODO (M6, spec §05): success toast on delete.
if (ok) remove(dataset.id);
if (!ok) return;
const removedName = dataset.name;
remove(dataset.id);
// Confirm the deletion (spec §05). The message names which dataset went
// (council toast-copy rule, docs/architecture/10 → Toast copy).
notify({
kind: 'success',
title: 'Dataset deleted',
message: `"${removedName}" was permanently removed.`,
});
};
return (
@@ -203,6 +211,13 @@ function DatasetDetail({
<button type="button" className={styles.action} onClick={() => void handleCopy()}>
{copied ? 'Copied' : 'Copy Reference'}
</button>
{/* The clipboard write is invisible, so the success is confirmed inline
("Copied") rather than by a toast (docs/architecture/10 → Toast copy).
This polite live region announces it to assistive tech, which the
button's visual label swap alone would not reliably do. */}
<span role="status" className="visually-hidden">
{copied ? 'Reference copied to clipboard' : ''}
</span>
<button type="button" className={styles.action} onClick={handleEdit}>
Edit
</button>
+13 -2
View File
@@ -11,6 +11,7 @@
import { useShallow } from 'zustand/react/shallow';
import { formatSnippetSize, hasUnpublishedChanges, snippetSizeBytes } from '@core/snippet';
import { confirm } from '../stores/ConfirmStore';
import { notify } from '../stores/NotificationStore';
import { useSnippetStore } from '../stores/SnippetStore';
import styles from './SnippetLibrary.module.css';
@@ -73,18 +74,28 @@ export function SnippetLibrary() {
const handleDelete = async (id: string, name: string) => {
// In-app confirmation (docs/architecture/03 → confirmation dialogs).
// TODO: surface a deletion toast (spec §02) once the toast system lands (M6).
const ok = await confirm({
title: 'Delete snippet',
message: `Delete "${name}"? This cannot be undone.`,
confirmLabel: 'Delete',
danger: true,
});
if (ok) removeSnippet(id);
if (!ok) return;
removeSnippet(id);
// Confirm the deletion (spec §02). The message names which snippet went —
// useful when toasts stack (council toast-copy rule, docs/architecture/10).
notify({
kind: 'success',
title: 'Snippet deleted',
message: `"${name}" was permanently removed from your library.`,
});
};
return (
<div className={styles.library}>
{/* Create raises no toast: the new snippet opens in the editor, so the
result is already on-screen (spec §02; docs/architecture/10 → Toast
copy). Delete/duplicate toast because the outcome isn't visible. */}
<button className={styles.createNew} onClick={() => createSnippet()}>
+ Create New Snippet
</button>
+14 -2
View File
@@ -27,6 +27,7 @@ import { openModal } from '../modals/ModalCoordinator';
import { useAppStore } from '../stores/AppStore';
import { confirm } from '../stores/ConfirmStore';
import { hasInlineData } from '../stores/ExtractStore';
import { notify } from '../stores/NotificationStore';
import { usePreviewStore } from '../stores/PreviewStore';
import { selectActiveSnippet, selectShownText, useSnippetStore } from '../stores/SnippetStore';
import { SegmentedControl, type SegmentedOption } from './SegmentedControl';
@@ -64,7 +65,14 @@ function EditorToolbar() {
const handlePublish = () => {
if (!useSnippetStore.getState().activeSnippetId) return;
useSnippetStore.getState().publish();
// TODO: success toast "Snippet published" once the toast system lands (M6, spec §03D).
// Success confirmation (spec §03D). Per the council's toast-copy rule
// (docs/architecture/10 → Toast copy), the title states the action and the
// message adds the consequence rather than paraphrasing it.
notify({
kind: 'success',
title: 'Snippet published',
message: 'Your draft is now the published version.',
});
};
const handleRevert = async () => {
@@ -77,7 +85,11 @@ function EditorToolbar() {
});
if (ok) {
useSnippetStore.getState().revert();
// TODO: success toast "Draft reverted" once the toast system lands (M6, spec §03D).
notify({
kind: 'success',
title: 'Draft reverted',
message: 'The editor was restored to the last published version.',
});
}
};
+7 -3
View File
@@ -1,10 +1,14 @@
.region {
position: fixed;
top: var(--space-5);
/* Bottom-right, not top-right: the header's action cluster (Publish/Revert,
theme/datasets controls) lives top-right, and a toast there lands on top of
the control the user just used. Bottom-anchored, the stack grows upward and
the newest toast sits nearest the corner (docs/architecture/10 → Toasts). */
bottom: var(--space-5);
right: var(--space-5);
/* Above the confirm backdrop (z 1000) so a failure stays visible and
dismissible even with a confirmation open; top-right won't block the
centered dialog. */
dismissible even with a confirmation open; the corner clears the centered
dialog. */
z-index: 1100;
display: flex;
flex-direction: column;
+3 -2
View File
@@ -2,8 +2,9 @@
* Toaster — renders the NotificationStore as a stack of toasts (spec §10,
* design language → Toasts). Mounted once at the app root, beside ConfirmDialog.
*
* The non-blocking counterpart to ConfirmDialog: top-right, stacked newest-last,
* each dismissible. Error/warning toasts persist until dismissed (Carbon: a
* The non-blocking counterpart to ConfirmDialog: bottom-right, stacked
* newest-nearest-the-corner, each dismissible (placement rationale in
* Toaster.module.css). Error/warning toasts persist until dismissed (Carbon: a
* critical message shouldn't vanish on a timer); success/info auto-dismiss. A
* failure that carries diagnostic `detail` exposes it under a collapsed
* "Technical details" disclosure — available to report, without shouting.
+4
View File
@@ -221,6 +221,10 @@ export const useDatasetStore = create<DatasetState>((set, get) => ({
now,
});
get().add(dataset);
// Switching to the detail view with the new dataset selected IS the success
// confirmation, so no toast is raised — the result is on-screen (spec §05;
// docs/architecture/10 → Toast copy). Extract-to-dataset, which creates a
// dataset off-screen, does toast (see ExtractStore).
set({ view: 'detail', form: EMPTY_FORM, formError: null });
return true;
},
+9 -2
View File
@@ -17,6 +17,7 @@ import type { DataFormat } from '@core/format-detection';
import { createDataset } from '@core/dataset';
import { isNameTaken } from '@core/naming';
import { useDatasetStore } from './DatasetStore';
import { notify } from './NotificationStore';
import { useSnippetStore } from './SnippetStore';
/** The inline `data` block of a parsed spec, if it carries `values`. */
@@ -123,8 +124,14 @@ export const useExtractStore = create<ExtractState>((set, get) => ({
spec.data = { name };
useSnippetStore.getState().replaceActiveDraft(JSON.stringify(spec, null, 2), now);
// TODO (M6, spec §03F): success toast "Dataset created" — deferred with the
// other success toasts (see SnippetStore publish/revert breadcrumbs).
// Success confirmation (spec §03F). Title states the action; the message
// adds the consequence — the spec was rewritten to reference the new dataset
// by name (council toast-copy rule, docs/architecture/10 → Toast copy).
notify({
kind: 'success',
title: 'Dataset created',
message: `The spec now references "${name}" instead of its inline data.`,
});
set(INITIAL);
return true;
},