@elytracms/studio 0.0.98 → 0.0.100

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.
Files changed (95) hide show
  1. package/convex/cms.ts +29 -2
  2. package/convex/ingest.test.ts +84 -7
  3. package/convex/ingest.ts +110 -31
  4. package/convex/path-segment-refusal.test.ts +201 -0
  5. package/convex/records-column-transform.convex.test.ts +149 -0
  6. package/convex/records-one-write.convex.test.ts +218 -0
  7. package/convex/schema.ts +1 -1
  8. package/convex/version-kind-stamping.test.ts +118 -0
  9. package/dist/addon.d.ts +5 -3
  10. package/dist/addon.js +4 -3
  11. package/dist/addon.js.map +1 -1
  12. package/dist/components/app-views/default-modules.d.ts +10 -3
  13. package/dist/components/app-views/default-modules.js +5 -1
  14. package/dist/components/app-views/default-modules.js.map +1 -1
  15. package/dist/components/app-views/module.d.ts +15 -8
  16. package/dist/components/app-views/module.js.map +1 -1
  17. package/dist/components/builder/reuse-context.d.ts +29 -1
  18. package/dist/components/builder/reuse-context.js +58 -12
  19. package/dist/components/builder/reuse-context.js.map +1 -1
  20. package/dist/components/cms/cms-store.d.ts +3 -2
  21. package/dist/components/cms/cms-store.js +8 -2
  22. package/dist/components/cms/cms-store.js.map +1 -1
  23. package/dist/components/cms/field-control.js +15 -5
  24. package/dist/components/cms/field-control.js.map +1 -1
  25. package/dist/components/cms/use-document-draft.d.ts +4 -1
  26. package/dist/components/cms/use-document-draft.js +4 -1
  27. package/dist/components/cms/use-document-draft.js.map +1 -1
  28. package/dist/components/cms/use-publish-controller.d.ts +7 -0
  29. package/dist/components/cms/use-publish-controller.js +17 -3
  30. package/dist/components/cms/use-publish-controller.js.map +1 -1
  31. package/dist/components/cms/versions-pane.js +8 -5
  32. package/dist/components/cms/versions-pane.js.map +1 -1
  33. package/dist/components/delivery-health-banner.d.ts +34 -16
  34. package/dist/components/delivery-health-banner.js +59 -27
  35. package/dist/components/delivery-health-banner.js.map +1 -1
  36. package/dist/components/issue-severity.d.ts +3 -2
  37. package/dist/components/issue-severity.js.map +1 -1
  38. package/dist/components/nav-issues-registry.d.ts +39 -24
  39. package/dist/components/nav-issues-registry.js +50 -21
  40. package/dist/components/nav-issues-registry.js.map +1 -1
  41. package/dist/components/nav-issues.d.ts +23 -50
  42. package/dist/components/nav-issues.js +31 -59
  43. package/dist/components/nav-issues.js.map +1 -1
  44. package/dist/components/notifications/notice-center.d.ts +61 -0
  45. package/dist/components/notifications/notice-center.js +119 -0
  46. package/dist/components/notifications/notice-center.js.map +1 -0
  47. package/dist/components/notifications/notice-model.d.ts +137 -0
  48. package/dist/components/notifications/notice-model.js +107 -0
  49. package/dist/components/notifications/notice-model.js.map +1 -0
  50. package/dist/components/notifications/outcome-notice.d.ts +16 -0
  51. package/dist/components/notifications/outcome-notice.js +34 -0
  52. package/dist/components/notifications/outcome-notice.js.map +1 -0
  53. package/dist/components/notifications/shell-findings.d.ts +36 -0
  54. package/dist/components/notifications/shell-findings.js +140 -0
  55. package/dist/components/notifications/shell-findings.js.map +1 -0
  56. package/dist/components/rail-view-source.js +5 -0
  57. package/dist/components/rail-view-source.js.map +1 -1
  58. package/dist/components/records/record-rail.d.ts +10 -2
  59. package/dist/components/records/record-rail.js +85 -4
  60. package/dist/components/records/record-rail.js.map +1 -1
  61. package/dist/components/section-nav.d.ts +15 -3
  62. package/dist/components/section-nav.js +39 -147
  63. package/dist/components/section-nav.js.map +1 -1
  64. package/dist/components/studio-project-screen.js.map +1 -1
  65. package/dist/components/studio-shell.js +8 -3
  66. package/dist/components/studio-shell.js.map +1 -1
  67. package/dist/components/use-narrow-viewport.d.ts +11 -0
  68. package/dist/components/use-narrow-viewport.js +25 -0
  69. package/dist/components/use-narrow-viewport.js.map +1 -0
  70. package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
  71. package/dist/components/users/role-policy-drift-issues.js +17 -8
  72. package/dist/components/users/role-policy-drift-issues.js.map +1 -1
  73. package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
  74. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  75. package/dist/lib/engine-version.d.ts +1 -1
  76. package/dist/lib/engine-version.js +1 -1
  77. package/dist/lib/engine-version.js.map +1 -1
  78. package/dist/lib/i18n/messages.de.js +22 -3
  79. package/dist/lib/i18n/messages.de.js.map +1 -1
  80. package/dist/lib/i18n/messages.en.d.ts +20 -2
  81. package/dist/lib/i18n/messages.en.js +25 -4
  82. package/dist/lib/i18n/messages.en.js.map +1 -1
  83. package/dist/lib/notices/toast-store.d.ts +61 -0
  84. package/dist/lib/notices/toast-store.js +116 -0
  85. package/dist/lib/notices/toast-store.js.map +1 -0
  86. package/dist/lib/operations/cms-persistence.d.ts +24 -4
  87. package/dist/lib/operations/cms-persistence.js +411 -80
  88. package/dist/lib/operations/cms-persistence.js.map +1 -1
  89. package/dist/lib/persistence/convex-adapter.d.ts +2 -0
  90. package/dist/lib/persistence/convex-adapter.js +127 -31
  91. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  92. package/dist/ui/toast.d.ts +24 -16
  93. package/dist/ui/toast.js +23 -32
  94. package/dist/ui/toast.js.map +1 -1
  95. package/package.json +3 -3
@@ -1,5 +1,12 @@
1
1
  import type { DocumentRef } from '@elytracms/core/cms-core';
2
2
  import type { CollectionViewSource } from '../../lib/collection-view/collection-view-source.js';
3
+ /**
4
+ * EC-690 — the toast a failed publish raises, or `null` when its owner already reported it.
5
+ * A REFUSAL (`PublishRefusedError`) is the session's `persistError` — an error notice of the
6
+ * issues tray that stays until dismissed — so a toast would list it twice. A failure nobody
7
+ * reported (a throw before or outside the operation) is still toasted.
8
+ */
9
+ export declare function publishFailureToast(error: unknown): string | null;
3
10
  export type PublishState = 'idle' | 'pending' | 'success';
4
11
  /**
5
12
  * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
@@ -2,6 +2,20 @@ import { useCallback, useRef, useState } from 'react';
2
2
  import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
3
3
  import { toast } from '@elytracms/studio/ui';
4
4
  import { translate } from '../../lib/i18n/index.js';
5
+ import { PublishRefusedError } from '../../lib/operations/publish-with-revision.js';
6
+ /**
7
+ * EC-690 — the toast a failed publish raises, or `null` when its owner already reported it.
8
+ * A REFUSAL (`PublishRefusedError`) is the session's `persistError` — an error notice of the
9
+ * issues tray that stays until dismissed — so a toast would list it twice. A failure nobody
10
+ * reported (a throw before or outside the operation) is still toasted.
11
+ */
12
+ export function publishFailureToast(error) {
13
+ if (error instanceof PublishRefusedError)
14
+ return null;
15
+ return error instanceof Error
16
+ ? translate('cms.editor.publishFailedReason', { error: error.message })
17
+ : translate('cms.editor.publishFailed');
18
+ }
5
19
  /**
6
20
  * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the
7
21
  * primary "Publish" and the "Publish & close" item can never diverge. A pure
@@ -65,9 +79,9 @@ export function usePublishController(input) {
65
79
  catch (error) {
66
80
  closeAfterPublishRef.current = false;
67
81
  setPublishState('idle');
68
- toast.error(error instanceof Error
69
- ? translate('cms.editor.publishFailedReason', { error: error.message })
70
- : translate('cms.editor.publishFailed'));
82
+ const message = publishFailureToast(error);
83
+ if (message !== null)
84
+ toast.error(message);
71
85
  }
72
86
  }, [flushDraft, source, docRef, navigate, projectId]);
73
87
  // Schema errors (strict-target errors included, EC-589) disable the button, so they
@@ -1 +1 @@
1
- {"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '../../lib/collection-view/collection-view-source'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Publish the draft. */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n}\n\n/**\n * EC-268 / 0.0.39 — the publish controller. Owns the publish indicator state and the\n * 0.0.39 \"Publish & close\" navigation (a successful publish returns to the rail-owned\n * list).\n *\n * EC-589: the EC-300 confirm-through for unpublished reuse embeds is gone. Integrity is\n * two-state and declared: a STRICT relation to an unpublished target is a blocking\n * validation error (the reuse embed declares `strict: true` on its relation prop), so\n * the publish button is disabled by the same issues that gate everything else — and\n * `documents.publish` refuses it for any client; a weak one publishes with a warning.\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n projectId: string\n /** EC-595 A2 — the editor's FORCED FLUSH (commit everything uncommitted, await persistence). */\n flushDraft: () => Promise<void>\n}): PublishController {\n const { source, docRef, projectId, flushDraft } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n // The split-button \"Publish & close\" intent, so a successful publish returns to the\n // list. Cleared on failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n setPublishState('pending')\n try {\n // EC-595 A2: the FORCED FLUSH — publish validates the COMMITTED draft, so every\n // uncommitted field commits and lands first.\n await flushDraft()\n // EC-585: the hidden-clear (field values, object sub-fields, component props —\n // into the draft AND the snapshot, judged in the project's locale views) runs\n // INSIDE `documents.publish`, for this and every other publish path. A refusal\n // rejects here (`PublishRefusedError`), so the indicator never says \"Published\"\n // for a publish that did not land.\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n }\n }, [flushDraft, source, docRef, navigate, projectId])\n\n // Schema errors (strict-target errors included, EC-589) disable the button, so they\n // never reach here.\n const handlePublish = useCallback(() => {\n void doPublish()\n }, [doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n return {\n publishState,\n handlePublish,\n handlePublishAndClose,\n }\n}\n"]}
1
+ {"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '../../lib/collection-view/collection-view-source'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '../../lib/i18n'\nimport { PublishRefusedError } from '../../lib/operations/publish-with-revision'\n\n/**\n * EC-690 — the toast a failed publish raises, or `null` when its owner already reported it.\n * A REFUSAL (`PublishRefusedError`) is the session's `persistError` — an error notice of the\n * issues tray that stays until dismissed — so a toast would list it twice. A failure nobody\n * reported (a throw before or outside the operation) is still toasted.\n */\nexport function publishFailureToast(error: unknown): string | null {\n if (error instanceof PublishRefusedError) return null\n return error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed')\n}\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Publish the draft. */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n}\n\n/**\n * EC-268 / 0.0.39 — the publish controller. Owns the publish indicator state and the\n * 0.0.39 \"Publish & close\" navigation (a successful publish returns to the rail-owned\n * list).\n *\n * EC-589: the EC-300 confirm-through for unpublished reuse embeds is gone. Integrity is\n * two-state and declared: a STRICT relation to an unpublished target is a blocking\n * validation error (the reuse embed declares `strict: true` on its relation prop), so\n * the publish button is disabled by the same issues that gate everything else — and\n * `documents.publish` refuses it for any client; a weak one publishes with a warning.\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n projectId: string\n /** EC-595 A2 — the editor's FORCED FLUSH (commit everything uncommitted, await persistence). */\n flushDraft: () => Promise<void>\n}): PublishController {\n const { source, docRef, projectId, flushDraft } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n // The split-button \"Publish & close\" intent, so a successful publish returns to the\n // list. Cleared on failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n setPublishState('pending')\n try {\n // EC-595 A2: the FORCED FLUSH — publish validates the COMMITTED draft, so every\n // uncommitted field commits and lands first.\n await flushDraft()\n // EC-585: the hidden-clear (field values, object sub-fields, component props —\n // into the draft AND the snapshot, judged in the project's locale views) runs\n // INSIDE `documents.publish`, for this and every other publish path. A refusal\n // rejects here (`PublishRefusedError`), so the indicator never says \"Published\"\n // for a publish that did not land.\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n const message = publishFailureToast(error)\n if (message !== null) toast.error(message)\n }\n }, [flushDraft, source, docRef, navigate, projectId])\n\n // Schema errors (strict-target errors included, EC-589) disable the button, so they\n // never reach here.\n const handlePublish = useCallback(() => {\n void doPublish()\n }, [doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n return {\n publishState,\n handlePublish,\n handlePublishAndClose,\n }\n}\n"]}
@@ -11,6 +11,7 @@ import { useDocumentEditor } from './document-editor-context.js';
11
11
  import { useDocumentVersionsQuery } from './use-document-versions.js';
12
12
  import { useRestoreVersion } from './use-restore-version.js';
13
13
  import { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model.js';
14
+ import { publishFailureToast } from './use-publish-controller.js';
14
15
  /**
15
16
  * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180
16
17
  * VersionsPanel at the document level (the Sanity model): the continuous draft
@@ -82,11 +83,13 @@ export function VersionsPane() {
82
83
  await cache.invalidate(versionKey);
83
84
  }
84
85
  catch (error) {
85
- // EC-585: a refused publish rejects (it is also on the persist banner) — say so
86
- // here too rather than let the rejection escape the click handler unhandled.
87
- toast.error(error instanceof Error
88
- ? translate('cms.editor.publishFailedReason', { error: error.message })
89
- : translate('cms.editor.publishFailed'));
86
+ // EC-585: a refused publish rejects — caught here so it never escapes the click
87
+ // handler. EC-690: a REFUSAL is already the session's `persistError` (an error notice of
88
+ // the issues tray that stays until dismissed), so it is not toasted a second time; a
89
+ // failure nobody reported still is (the shared decision, `publishFailureToast`).
90
+ const message = publishFailureToast(error);
91
+ if (message !== null)
92
+ toast.error(message);
90
93
  }
91
94
  finally {
92
95
  setBusy(false);
@@ -1 +1 @@
1
- {"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '../../lib/query'\nimport { translate, useUiLocale } from '../../lib/i18n'\nimport { cn } from '../../lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } catch (error) {\n // EC-585: a refused publish rejects (it is also on the persist banner) — say so\n // here too rather than let the rejection escape the click handler unhandled.\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
1
+ {"version":3,"file":"versions-pane.js","sourceRoot":"","sources":["../../../src/components/cms/versions-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { KeyboardEvent } from 'react'\nimport { Eye, GitCompareArrows, MoreHorizontal, RotateCcw } from 'lucide-react'\nimport {\n Badge,\n Button,\n Code,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n StatusDot,\n Text,\n toast,\n} from '@elytracms/studio/ui'\nimport { documentsSemanticEqual } from '@elytracms/core/content'\nimport { useOperationCache } from '../../lib/query'\nimport { translate, useUiLocale } from '../../lib/i18n'\nimport { cn } from '../../lib/utils'\nimport { useCollectionView } from '../collection-view/collection-view-context'\nimport { useDocumentEditor } from './document-editor-context'\nimport { useDocumentVersionsQuery } from './use-document-versions'\nimport { useRestoreVersion } from './use-restore-version'\nimport { formatRelativeVersionTimestamp, formatVersionTimestamp } from './versions-model'\nimport { publishFailureToast } from './use-publish-controller'\n\n/**\n * EC-224 — the document-level **Versions** inspector pane. Re-homes the EC-180\n * VersionsPanel at the document level (the Sanity model): the continuous draft\n * on top with **Publish**, then the recorded version history (the live pin\n * badged), each older version **restorable** into the draft.\n *\n * Publish routes through the same pointer op as the header control\n * (`cms.publishDocument`); restore loads a recorded snapshot as the new working\n * draft (the operation snapshots the current draft first, so a rollback is\n * itself undoable) and the open editor adopts it via `replaceDraft`. Unpublish\n * deliberately stays in the header — it routes through the EC-173\n * reference-aware confirmation, which this pane does not duplicate.\n *\n * EC-298: each row also opens the compare view (⤢) and, once comparing, doubles\n * as the navigator — clicking a row sets it as the base of the diff.\n */\nexport function VersionsPane() {\n const source = useCollectionView()\n const {\n docRef,\n draft,\n dirty,\n hasErrors,\n viewReadOnly,\n save,\n replaceDraft,\n compare,\n compareActive,\n startCompare,\n setCompareBase,\n } = useDocumentEditor()\n const [busy, setBusy] = useState(false)\n const published = source.publishing?.isPublished(docRef) ?? false\n const cache = useOperationCache()\n // EC-446 — the active studio locale + a render-time clock, so each row can show\n // a locale-aware RELATIVE timestamp (\"vor 3 Tagen\") with the absolute value on\n // hover. `now` is captured per render (good enough — the list re-renders on any\n // version mutation; a version's age doesn't need per-second reactivity).\n const locale = useUiLocale()\n const now = Date.now()\n\n // The recorded version history, read through the one shared query (EC-308/AD-17,\n // deduped with the compare view). The flush-before-read ordering lives in the\n // loader, so the listed history is authoritative regardless of which surface\n // triggered the change; it refreshes on any version-mutating command\n // (publish/unpublish/snapshot/restore/pin/delete — EC-322) from anywhere, but\n // NOT on a plain draft save (which appends no version). Editor draft/dirty/busy\n // stays local — it is not server state (principle 12).\n const { versionsQuery, versionKey } = useDocumentVersionsQuery(docRef)\n const view = versionsQuery.data ?? null\n // EC-325 — the shared restore sequence (flush → restore → replaceDraft →\n // invalidate). Its `restoreBusy` is OR-ed with the local publish `busy` below so\n // the pane keeps its original single-in-flight gate: while EITHER a publish or a\n // restore is round-tripping, all of the pane's actions disable together.\n const { restore: restoreVersion, busy: restoreBusy } = useRestoreVersion({\n source,\n docRef,\n versionKey,\n dirty,\n save,\n replaceDraft,\n })\n const anyBusy = busy || restoreBusy\n\n const liveSnapshot =\n view?.publishedVersion !== undefined\n ? view.versions.find((v) => v.version === view.publishedVersion)?.snapshot\n : undefined\n // SEMANTIC equality (EC-298) — the same engine the compare view uses, so the two\n // never contradict: a doc that only differs by EC-277 envelope drift reads as\n // \"up to date\", not \"unpublished changes\".\n const liveMatchesDraft =\n liveSnapshot !== undefined && documentsSemanticEqual(liveSnapshot, draft)\n const canPublish = !liveMatchesDraft\n\n const handlePublish = useCallback(async () => {\n setBusy(true)\n try {\n // Publish snapshots the SAVED draft — capture any unsaved edits first so\n // the published version is exactly what the editor shows. Await the\n // publish (it flushes + reconciles the pin, EC-225) before re-reading the\n // version list so the new version shows.\n if (dirty) save()\n await source.publishing?.publish(docRef)\n await cache.invalidate(versionKey)\n } catch (error) {\n // EC-585: a refused publish rejects — caught here so it never escapes the click\n // handler. EC-690: a REFUSAL is already the session's `persistError` (an error notice of\n // the issues tray that stays until dismissed), so it is not toasted a second time; a\n // failure nobody reported still is (the shared decision, `publishFailureToast`).\n const message = publishFailureToast(error)\n if (message !== null) toast.error(message)\n } finally {\n setBusy(false)\n }\n }, [source, docRef, dirty, save, cache, versionKey])\n\n // Restore a recorded version into the working draft — the whole-document\n // restore (no scope) via the shared EC-325 helper, which flushes any unsaved\n // buffer FIRST so the operation's pre-restore snapshot records it (else the\n // rollback wouldn't be undoable — restore snapshots the persisted draft).\n const handleRestore = useCallback(\n (version: number) => restoreVersion(version),\n [restoreVersion],\n )\n\n return (\n <Stack gap=\"3\" className=\"p-3\">\n {canPublish ? (\n <Row gap=\"2\" align=\"center\" className=\"rounded-md border border-primary/40 bg-primary/5 px-2.5 py-2\">\n <Stack gap=\"none\" className=\"min-w-0\">\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.editor.draftState')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {published\n ? translate('cms.version.unpublishedChanges')\n : translate('cms.version.notPublishedYet')}\n </Text>\n </Stack>\n <Spacer />\n <Button\n size=\"sm\"\n onClick={handlePublish}\n disabled={anyBusy || hasErrors || viewReadOnly}\n title={\n viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : hasErrors\n ? translate('cms.version.fixValidationToPublish')\n : undefined\n }\n >\n <Eye /> {translate('cms.editor.publish')}\n </Button>\n </Row>\n ) : (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.liveUpToDate')}\n </Text>\n )}\n\n {compareActive && compare ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.comparingHint', {\n base: compare.base,\n target:\n compare.target === 'draft'\n ? translate('cms.version.currentDraft')\n : `v${compare.target}`,\n })}\n </Text>\n ) : null}\n\n <Stack gap=\"1\">\n {view === null ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('common.loading')}\n </Text>\n ) : view.versions.length === 0 ? (\n <Text size=\"xs\" tone=\"muted\" className=\"px-0.5\">\n {translate('cms.version.noVersionsYet')}\n </Text>\n ) : (\n view.versions.map((entry) => {\n const isLive = entry.version === view.publishedVersion\n const isBase = compareActive && compare?.base === entry.version\n const isTarget = compareActive && compare?.target === entry.version\n const selectable = compareActive && !isTarget\n return (\n <Row\n key={entry.version}\n gap=\"2\"\n align=\"center\"\n {...(selectable\n ? {\n role: 'button',\n tabIndex: 0,\n onClick: () => setCompareBase(entry.version),\n onKeyDown: (e: KeyboardEvent) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n setCompareBase(entry.version)\n }\n },\n }\n : {})}\n className={cn(\n 'rounded-md border px-2.5 py-1.5',\n isBase ? 'border-primary bg-primary/5' : 'border-border/60',\n selectable && !isBase && 'cursor-pointer hover:bg-muted/60',\n )}\n >\n {/* EC-446 — the live/published version leads with a green STATUS\n DOT (hover/focus reveals \"Published\"); historical versions\n reserve the same footprint so the version codes stay aligned. */}\n {isLive ? (\n <StatusDot tone=\"success\" label={translate('cms.editor.publishedState')} />\n ) : (\n <span aria-hidden className=\"size-2 shrink-0\" />\n )}\n <Code tone={isLive ? 'primary' : 'muted'}>v{entry.version}</Code>\n {isBase ? (\n <Badge variant=\"outline\">{translate('cms.version.baseBadge')}</Badge>\n ) : isTarget ? (\n <Badge variant=\"outline\">{translate('cms.version.comparedBadge')}</Badge>\n ) : null}\n {entry.origin === 'assistant' ? (\n <Badge variant=\"outline\" title={translate('cms.version.aiAuthoredTitle')}>\n {translate('cms.version.aiAuthored')}\n </Badge>\n ) : null}\n {/* EC-446 — readable RELATIVE date; the full absolute timestamp\n on hover (`title`), replacing the old truncated stub. */}\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"truncate\"\n title={formatVersionTimestamp(entry.createdAt)}\n >\n {formatRelativeVersionTimestamp(entry.createdAt, now, locale)}\n </Text>\n <Spacer />\n {/* EC-446 — actions collapse into a per-row `⋯` overflow menu\n (mirrors the collection-table row menu). Suppressed in compare\n mode exactly as the buttons were, so the row stays a base picker. */}\n {compareActive ? null : (\n <VersionRowActions\n version={entry.version}\n isLive={isLive}\n restoreDisabled={anyBusy || viewReadOnly}\n restoreDisabledTitle={\n viewReadOnly ? translate('cms.version.switchToDraftToRestore') : undefined\n }\n onCompare={() => startCompare(entry.version)}\n onRestore={() => void handleRestore(entry.version)}\n />\n )}\n </Row>\n )\n })\n )}\n </Stack>\n </Stack>\n )\n}\n\n/**\n * EC-446 — one version row's `⋯` overflow menu, mirroring the collection-table\n * row-actions menu (`RowActionsMenu`, built on the same `DropdownMenu`\n * primitive). Compare (⤢) is always offered; Restore (↺) is offered on every\n * version EXCEPT the live/published one (which is already the draft's baseline —\n * matching the pre-redesign behavior). The live version therefore shows a\n * Compare-only menu, never a bare/empty trigger.\n *\n * Restore keeps the pane's original gates: `restoreDisabled` (the single\n * in-flight gate `anyBusy` OR `viewReadOnly`) disables the item, and the\n * read-only hint rides its `title`.\n */\nfunction VersionRowActions({\n version,\n isLive,\n restoreDisabled,\n restoreDisabledTitle,\n onCompare,\n onRestore,\n}: {\n version: number\n isLive: boolean\n restoreDisabled: boolean\n restoreDisabledTitle: string | undefined\n onCompare: () => void\n onRestore: () => void\n}) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n aria-label={translate('cms.version.rowActionsLabel', { version })}\n data-testid={`version-actions-${version}`}\n >\n <MoreHorizontal />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\">\n <DropdownMenuItem\n onClick={onCompare}\n data-testid={`version-action-compare-${version}`}\n >\n <GitCompareArrows /> {translate('cms.version.compare')}\n </DropdownMenuItem>\n {isLive ? null : (\n <DropdownMenuItem\n onClick={onRestore}\n disabled={restoreDisabled}\n {...(restoreDisabledTitle ? { title: restoreDisabledTitle } : {})}\n data-testid={`version-action-restore-${version}`}\n >\n <RotateCcw /> {translate('cms.version.restore')}\n </DropdownMenuItem>\n )}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
@@ -1,18 +1,36 @@
1
+ import type { DeliveryConfigStatus } from '@elytracms/core/persistence';
2
+ import type { FindingAction } from './notifications/notice-model.js';
3
+ import type { NavIssueItem } from './nav-issues.js';
4
+ /** A delivery-health finding: an engine finding, so it may carry a Retry. */
5
+ export type DeliveryHealthFinding = NavIssueItem & {
6
+ action?: FindingAction;
7
+ };
1
8
  /**
2
- * EC-268: a boot-up health check for instant publish (EC-146). When the studio is
3
- * connected to a Convex deployment, it reads the delivery config through the
4
- * operations seam (EC-329 — `operations.delivery.config`, cached by
5
- * `useOperationQuery` like every other studio read, AD-17) and, if any of the
6
- * delivery env vars are unset, shows a dismissible warning. Without this the
7
- * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
8
- * returns before scheduling (no `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and
9
- * the live site never updates. In playground mode (no backend) instant publish does
10
- * not apply — no host reads the in-browser adapter, so there is no emitter — and the
11
- * banner never renders (the query is disabled, so it never has data). A failed
12
- * read degrades silently — it must never block the studio — by treating the
13
- * query's error branch the same as "nothing missing", matching the former raw
14
- * client call's swallowed `.catch()`.
9
+ * The pure decision (EC-690): what instant-publish health reports, from the query's state.
10
+ *
11
+ * - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;
12
+ * - data with missing vars → ONE warning naming them, located at the deployment;
13
+ * - NO data and an error → one warning "could not be checked", with a Retry. A failed read
14
+ * used to be swallowed as "nothing missing" (a silent failure, AGENTS §4); a failed
15
+ * RE-read while earlier data is held reports from that data instead (critic point 7: the
16
+ * query does not retry on its own, so one transient error must not stick for the session).
15
17
  */
16
- export declare function DeliveryHealthBanner({ projectId }: {
17
- projectId: string;
18
- }): import("react").JSX.Element | null;
18
+ export declare function deliveryHealthFindings(input: {
19
+ checked: boolean;
20
+ data: DeliveryConfigStatus | undefined;
21
+ error: unknown;
22
+ retry: () => void;
23
+ }): DeliveryHealthFinding[];
24
+ /**
25
+ * EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the
26
+ * one issues tray (it used to be a banner above the rail with a per-session dismiss; the
27
+ * owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).
28
+ *
29
+ * When the studio is connected to a Convex deployment it reads the delivery config through
30
+ * the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`
31
+ * like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish
32
+ * reports success but `scheduleRevalidate` returns before scheduling (no
33
+ * `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In
34
+ * playground mode (no backend) instant publish does not apply, so the query is disabled.
35
+ */
36
+ export declare function useDeliveryHealthFindings(projectId: string): DeliveryHealthFinding[];
@@ -1,29 +1,65 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from 'react';
3
- import { Rocket, X } from 'lucide-react';
4
- import { Button, Row, Spacer, Text } from '@elytracms/studio/ui';
1
+ import { useMemo } from 'react';
5
2
  import { translate } from '../lib/i18n/index.js';
6
3
  import { resolveBackendConfig } from '../lib/persistence/backend-mode.js';
7
4
  import { missingInstantPublishVars } from '../lib/delivery/instant-publish.js';
8
5
  import { useOperationQuery } from '../lib/query/index.js';
6
+ import { PROJECT_CONFIG_FILE, configProjectFor } from '../lib/workspace/index.js';
7
+ /** The Convex settings the finding names (`INSTANT_PUBLISH_VARS`), for a never-linked location. */
8
+ const deploymentAt = (settings) => ({ kind: 'deployment', settings });
9
9
  /**
10
- * EC-268: a boot-up health check for instant publish (EC-146). When the studio is
11
- * connected to a Convex deployment, it reads the delivery config through the
12
- * operations seam (EC-329 — `operations.delivery.config`, cached by
13
- * `useOperationQuery` like every other studio read, AD-17) and, if any of the
14
- * delivery env vars are unset, shows a dismissible warning. Without this the
15
- * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`
16
- * returns before scheduling (no `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and
17
- * the live site never updates. In playground mode (no backend) instant publish does
18
- * not apply — no host reads the in-browser adapter, so there is no emitter — and the
19
- * banner never renders (the query is disabled, so it never has data). A failed
20
- * read degrades silently — it must never block the studio — by treating the
21
- * query's error branch the same as "nothing missing", matching the former raw
22
- * client call's swallowed `.catch()`.
10
+ * The pure decision (EC-690): what instant-publish health reports, from the query's state.
11
+ *
12
+ * - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;
13
+ * - data with missing vars → ONE warning naming them, located at the deployment;
14
+ * - NO data and an error → one warning "could not be checked", with a Retry. A failed read
15
+ * used to be swallowed as "nothing missing" (a silent failure, AGENTS §4); a failed
16
+ * RE-read while earlier data is held reports from that data instead (critic point 7: the
17
+ * query does not retry on its own, so one transient error must not stick for the session).
23
18
  */
24
- export function DeliveryHealthBanner({ projectId }) {
25
- const [dismissed, setDismissed] = useState(false);
26
- const enabled = resolveBackendConfig().kind === 'convex';
19
+ export function deliveryHealthFindings(input) {
20
+ if (!input.checked)
21
+ return [];
22
+ if (input.data) {
23
+ const missing = missingInstantPublishVars(input.data);
24
+ if (missing.length === 0)
25
+ return [];
26
+ return [
27
+ {
28
+ key: 'instant-publish-missing',
29
+ severity: 'warning',
30
+ title: translate('shell.deliveryHealth.title'),
31
+ message: translate('shell.deliveryHealth.missing', { vars: missing.join(', '), file: PROJECT_CONFIG_FILE }),
32
+ location: deploymentAt(missing),
33
+ },
34
+ ];
35
+ }
36
+ if (input.error === null || input.error === undefined)
37
+ return [];
38
+ const reason = input.error instanceof Error ? input.error.message : String(input.error);
39
+ return [
40
+ {
41
+ key: 'instant-publish-unchecked',
42
+ severity: 'warning',
43
+ title: translate('shell.deliveryHealth.title'),
44
+ message: translate('shell.deliveryHealth.unchecked', { error: reason }),
45
+ action: { label: translate('common.retry'), run: input.retry },
46
+ },
47
+ ];
48
+ }
49
+ /**
50
+ * EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the
51
+ * one issues tray (it used to be a banner above the rail with a per-session dismiss; the
52
+ * owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).
53
+ *
54
+ * When the studio is connected to a Convex deployment it reads the delivery config through
55
+ * the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`
56
+ * like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish
57
+ * reports success but `scheduleRevalidate` returns before scheduling (no
58
+ * `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In
59
+ * playground mode (no backend) instant publish does not apply, so the query is disabled.
60
+ */
61
+ export function useDeliveryHealthFindings(projectId) {
62
+ const checked = resolveBackendConfig().kind === 'convex' && configProjectFor(projectId)?.instantPublish !== false;
27
63
  const query = useOperationQuery({
28
64
  queryKey: ['delivery', 'config', projectId],
29
65
  loader: async (operations) => {
@@ -32,13 +68,9 @@ export function DeliveryHealthBanner({ projectId }) {
32
68
  throw new Error(result.error.message);
33
69
  return result.value;
34
70
  },
35
- enabled,
71
+ enabled: checked,
36
72
  });
37
- // Loading/error/playground (disabled) all read as "nothing missing" — the
38
- // banner only ever surfaces a CONFIRMED gap, never a transient/unreachable one.
39
- const missing = query.data ? missingInstantPublishVars(query.data) : [];
40
- if (dismissed || missing.length === 0)
41
- return null;
42
- return (_jsxs(Row, { gap: "2", className: "shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6", "data-testid": "delivery-health-banner", children: [_jsx(Rocket, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsxs(Text, { size: "xs", className: "text-warning", children: ["Instant publish isn\u2019t fully configured \u2014 ", missing.join(', '), " not set on this Convex deployment. Publishes succeed, but the live site won\u2019t update until you set them (see CONNECT.md) and re-push functions."] }), _jsx(Spacer, {}), _jsx(Button, { variant: "ghost", size: "icon-sm", onClick: () => setDismissed(true), "aria-label": translate('shell.deliveryHealth.dismiss'), title: translate('shell.deliveryHealth.dismiss'), children: _jsx(X, { "aria-hidden": true }) })] }));
73
+ const { data, error, refetch } = query;
74
+ return useMemo(() => deliveryHealthFindings({ checked, data, error, retry: () => void refetch() }), [checked, data, error, refetch]);
43
75
  }
44
76
  //# sourceMappingURL=delivery-health-banner.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AACxC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAChE,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AACvC,OAAO,EAAE,oBAAoB,EAAE,MAAM,iCAAiC,CAAA;AACtE,OAAO,EAAE,yBAAyB,EAAE,MAAM,iCAAiC,CAAA;AAC3E,OAAO,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAA;AAGhD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,oBAAoB,CAAC,EAAE,SAAS,EAAyB;IACvE,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACjD,MAAM,OAAO,GAAG,oBAAoB,EAAE,CAAC,IAAI,KAAK,QAAQ,CAAA;IAExD,MAAM,KAAK,GAAG,iBAAiB,CAAuB;QACpD,QAAQ,EAAE,CAAC,UAAU,EAAE,QAAQ,EAAE,SAAS,CAAC;QAC3C,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE;YAC3B,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC,CAAA;YAC9D,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,OAAO,CAAC,CAAA;YACrD,OAAO,MAAM,CAAC,KAAK,CAAA;QACrB,CAAC;QACD,OAAO;KACR,CAAC,CAAA;IACF,0EAA0E;IAC1E,gFAAgF;IAChF,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,yBAAyB,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;IAEvE,IAAI,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAElD,OAAO,CACL,MAAC,GAAG,IACF,GAAG,EAAC,GAAG,EACP,SAAS,EAAC,oFAAoF,iBAClF,wBAAwB,aAEpC,KAAC,MAAM,IAAC,SAAS,EAAC,mBAAmB,wBAAe,EACpD,MAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,SAAS,EAAC,cAAc,oEACI,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,6JAGvD,EACP,KAAC,MAAM,KAAG,EACV,KAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,gBACrB,SAAS,CAAC,8BAA8B,CAAC,EACrD,KAAK,EAAE,SAAS,CAAC,8BAA8B,CAAC,YAEhD,KAAC,CAAC,0BAAe,GACV,IACL,CACP,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { Rocket, X } from 'lucide-react'\nimport { Button, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { translate } from '../lib/i18n'\nimport { resolveBackendConfig } from '../lib/persistence/backend-mode'\nimport { missingInstantPublishVars } from '../lib/delivery/instant-publish'\nimport { useOperationQuery } from '../lib/query'\nimport type { DeliveryConfigStatus } from '@elytracms/core/persistence'\n\n/**\n * EC-268: a boot-up health check for instant publish (EC-146). When the studio is\n * connected to a Convex deployment, it reads the delivery config through the\n * operations seam (EC-329 — `operations.delivery.config`, cached by\n * `useOperationQuery` like every other studio read, AD-17) and, if any of the\n * delivery env vars are unset, shows a dismissible warning. Without this the\n * pipeline fails SILENTLY — a publish reports success but `scheduleRevalidate`\n * returns before scheduling (no `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and\n * the live site never updates. In playground mode (no backend) instant publish does\n * not apply — no host reads the in-browser adapter, so there is no emitter — and the\n * banner never renders (the query is disabled, so it never has data). A failed\n * read degrades silently — it must never block the studio — by treating the\n * query's error branch the same as \"nothing missing\", matching the former raw\n * client call's swallowed `.catch()`.\n */\nexport function DeliveryHealthBanner({ projectId }: { projectId: string }) {\n const [dismissed, setDismissed] = useState(false)\n const enabled = resolveBackendConfig().kind === 'convex'\n\n const query = useOperationQuery<DeliveryConfigStatus>({\n queryKey: ['delivery', 'config', projectId],\n loader: async (operations) => {\n const result = await operations.delivery.config({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n },\n enabled,\n })\n // Loading/error/playground (disabled) all read as \"nothing missing\" — the\n // banner only ever surfaces a CONFIRMED gap, never a transient/unreachable one.\n const missing = query.data ? missingInstantPublishVars(query.data) : []\n\n if (dismissed || missing.length === 0) return null\n\n return (\n <Row\n gap=\"2\"\n className=\"shrink-0 border-b border-warning/30 bg-warning/10 px-4 py-1.5 text-warning sm:px-6\"\n data-testid=\"delivery-health-banner\"\n >\n <Rocket className=\"size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"text-warning\">\n Instant publish isn’t fully configured — {missing.join(', ')} not set on this Convex\n deployment. Publishes succeed, but the live site won’t update until you set them (see\n CONNECT.md) and re-push functions.\n </Text>\n <Spacer />\n <Button\n variant=\"ghost\"\n size=\"icon-sm\"\n onClick={() => setDismissed(true)}\n aria-label={translate('shell.deliveryHealth.dismiss')}\n title={translate('shell.deliveryHealth.dismiss')}\n >\n <X aria-hidden />\n </Button>\n </Row>\n )\n}\n"]}
1
+ {"version":3,"file":"delivery-health-banner.js","sourceRoot":"","sources":["../../src/components/delivery-health-banner.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo } from 'react'\nimport type { DeliveryConfigStatus } from '@elytracms/core/persistence'\nimport { translate } from '../lib/i18n'\nimport { resolveBackendConfig } from '../lib/persistence/backend-mode'\nimport { missingInstantPublishVars } from '../lib/delivery/instant-publish'\nimport { useOperationQuery } from '../lib/query'\nimport { PROJECT_CONFIG_FILE, configProjectFor } from '../lib/workspace'\nimport type { FindingAction, NoticeLocation } from './notifications/notice-model'\nimport type { NavIssueItem } from './nav-issues'\n\n/** A delivery-health finding: an engine finding, so it may carry a Retry. */\nexport type DeliveryHealthFinding = NavIssueItem & { action?: FindingAction }\n\n/** The Convex settings the finding names (`INSTANT_PUBLISH_VARS`), for a never-linked location. */\nconst deploymentAt = (settings: readonly string[]): NoticeLocation => ({ kind: 'deployment', settings })\n\n/**\n * The pure decision (EC-690): what instant-publish health reports, from the query's state.\n *\n * - opted out (`instantPublish: false` in `elytra.config.ts`) or not connected → nothing;\n * - data with missing vars → ONE warning naming them, located at the deployment;\n * - NO data and an error → one warning \"could not be checked\", with a Retry. A failed read\n * used to be swallowed as \"nothing missing\" (a silent failure, AGENTS §4); a failed\n * RE-read while earlier data is held reports from that data instead (critic point 7: the\n * query does not retry on its own, so one transient error must not stick for the session).\n */\nexport function deliveryHealthFindings(input: {\n checked: boolean\n data: DeliveryConfigStatus | undefined\n error: unknown\n retry: () => void\n}): DeliveryHealthFinding[] {\n if (!input.checked) return []\n if (input.data) {\n const missing = missingInstantPublishVars(input.data)\n if (missing.length === 0) return []\n return [\n {\n key: 'instant-publish-missing',\n severity: 'warning',\n title: translate('shell.deliveryHealth.title'),\n message: translate('shell.deliveryHealth.missing', { vars: missing.join(', '), file: PROJECT_CONFIG_FILE }),\n location: deploymentAt(missing),\n },\n ]\n }\n if (input.error === null || input.error === undefined) return []\n const reason = input.error instanceof Error ? input.error.message : String(input.error)\n return [\n {\n key: 'instant-publish-unchecked',\n severity: 'warning',\n title: translate('shell.deliveryHealth.title'),\n message: translate('shell.deliveryHealth.unchecked', { error: reason }),\n action: { label: translate('common.retry'), run: input.retry },\n },\n ]\n}\n\n/**\n * EC-268 → EC-690: the boot-up health check for instant publish (EC-146), as findings of the\n * one issues tray (it used to be a banner above the rail with a per-session dismiss; the\n * owner replaced the dismiss with the declared `instantPublish: false`, ruling (c)).\n *\n * When the studio is connected to a Convex deployment it reads the delivery config through\n * the operations seam (EC-329 — `operations.delivery.config`, cached by `useOperationQuery`\n * like every studio read, AD-17). Without this the pipeline fails SILENTLY — a publish\n * reports success but `scheduleRevalidate` returns before scheduling (no\n * `CANVAS_REVALIDATE_ENDPOINT`/`_SECRET` pair) and the live site never updates. In\n * playground mode (no backend) instant publish does not apply, so the query is disabled.\n */\nexport function useDeliveryHealthFindings(projectId: string): DeliveryHealthFinding[] {\n const checked =\n resolveBackendConfig().kind === 'convex' && configProjectFor(projectId)?.instantPublish !== false\n\n const query = useOperationQuery<DeliveryConfigStatus>({\n queryKey: ['delivery', 'config', projectId],\n loader: async (operations) => {\n const result = await operations.delivery.config({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return result.value\n },\n enabled: checked,\n })\n const { data, error, refetch } = query\n return useMemo(\n () => deliveryHealthFindings({ checked, data, error, retry: () => void refetch() }),\n [checked, data, error, refetch],\n )\n}\n"]}
@@ -2,8 +2,9 @@
2
2
  * EC-507 — the studio's ONE issue-severity treatment.
3
3
  *
4
4
  * Three surfaces render a finding by severity: the document Issues pane
5
- * (`components/cms/issues-pane.tsx`, EC-283), the section-rail foot
6
- * (`components/nav-issues.tsx`, EC-493) and the AI proposal card
5
+ * (`components/cms/issues-pane.tsx`, EC-283), the shell's issues tray
6
+ * (`components/notifications/notice-center.tsx`, EC-690 — it was the section-rail foot,
7
+ * EC-493) and the AI proposal card
7
8
  * (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but
8
9
  * not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`
9
10
  * beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with
@@ -1 +1 @@
1
- {"version":3,"file":"issue-severity.js","sourceRoot":"","sources":["../../src/components/issue-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAiCzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAuB;IAMxD,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,wCAAwC,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC9E,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;AACtE,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,QAAuB;IACvD,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAA;AAC3D,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAkC;IACjD,OAAO,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAA;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA8C;IAE9C,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAoB;IAEpB,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { CircleAlert, TriangleAlert } from 'lucide-react'\n\n/**\n * EC-507 — the studio's ONE issue-severity treatment.\n *\n * Three surfaces render a finding by severity: the document Issues pane\n * (`components/cms/issues-pane.tsx`, EC-283), the section-rail foot\n * (`components/nav-issues.tsx`, EC-493) and the AI proposal card\n * (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but\n * not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`\n * beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with\n * nothing to do with navigation, and the original stayed inlined in the pane — three\n * expressions of one rule, and no obvious home for the fourth surface to import from.\n * This module is that home. The `nav*` names are retired.\n *\n * Deliberately NOT here: the label/copy vocabulary. `cmsIssueLabel`, the message\n * catalog and `issueBodyDescriptor` are the EC-192 persona boundary — a separate\n * concern that happens to be rendered by the same cards. Presentation and copy stay\n * apart.\n *\n * Also not here: the `data-*` attributes each surface emits\n * (`data-nav-issue-severity`, `data-proposal-issue-severity`). Those are per-surface\n * test/inspection hooks, not part of the shared rule.\n */\n\n/**\n * The two values every contributing producer publishes — `CmsValidationIssue.severity`,\n * `CustomizationIssue.severity` and the project-graph `ValidationIssue.severity` are all\n * `'error' | 'warning'`. A surface carries the field across rather than re-deriving a\n * local notion of \"bad\".\n */\nexport type IssueSeverity = 'error' | 'warning'\n\n/**\n * The per-severity palette, as a pure mapping onto the shared token palette —\n * unit-testable without a DOM (the `statusDotToneClass` pattern).\n *\n * The values are the document Issues pane's, which had them first: destructive\n * border/fill/text for an error, warning border/fill/text for a warning. A surface\n * applies `card` to the finding's container and `text` to at least its glyph; whether\n * the tint also carries to the body copy is the surface's call (the pane keeps its\n * body `muted`, the rail and the proposal card tint the whole row), because that is\n * layout, not severity.\n */\nexport function issueSeverityStyle(severity: IssueSeverity): {\n /** Border + fill for the finding's card. */\n card: string\n /** Icon + message tone. */\n text: string\n} {\n return severity === 'error'\n ? { card: 'border-destructive/30 bg-destructive/5', text: 'text-destructive' }\n : { card: 'border-warning/30 bg-warning/5', text: 'text-warning' }\n}\n\n/** The glyph for a severity — `CircleAlert` for an error, `TriangleAlert` for a warning. */\nexport function issueSeverityIcon(severity: IssueSeverity) {\n return severity === 'error' ? CircleAlert : TriangleAlert\n}\n\n/**\n * **The rank rule: `error` outranks `warning`.** Stated once here because two\n * surfaces express it differently — the collapsed rail shows one aggregate glyph\n * ({@link highestIssueSeverity}), the proposal card shows every finding and sorts\n * them ({@link orderBySeverity}) — and a rule expressed twice drifts.\n */\nfunction isError(issue: { severity: IssueSeverity }): boolean {\n return issue.severity === 'error'\n}\n\n/**\n * The severity ONE aggregate glyph should report for a mixed list: **the highest\n * present**.\n *\n * An icons-only rail may over-state the mildest finding, but it must never\n * under-report a hard error down to an advisory — the reader has no second signal\n * there, and expanding immediately corrects any over-statement.\n *\n * An empty list has no severity to report; callers do not render the widget at all\n * in that case, and `'warning'` is the conservative answer for one that does.\n */\nexport function highestIssueSeverity(\n issues: readonly { severity: IssueSeverity }[],\n): IssueSeverity {\n return issues.some(isError) ? 'error' : 'warning'\n}\n\n/**\n * The same rank as ordering: **errors first**, warnings after, stable within each\n * severity.\n *\n * For a surface that shows every finding rather than one glyph. Where the card's\n * primary action commits the change and the reader scans top-down, the finding that\n * should give pause must not sit below one that should merely inform.\n */\nexport function orderBySeverity<T extends { severity: IssueSeverity }>(\n issues: readonly T[],\n): T[] {\n return [...issues.filter(isError), ...issues.filter((issue) => !isError(issue))]\n}\n"]}
1
+ {"version":3,"file":"issue-severity.js","sourceRoot":"","sources":["../../src/components/issue-severity.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAkCzD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAAC,QAAuB;IAMxD,OAAO,QAAQ,KAAK,OAAO;QACzB,CAAC,CAAC,EAAE,IAAI,EAAE,wCAAwC,EAAE,IAAI,EAAE,kBAAkB,EAAE;QAC9E,CAAC,CAAC,EAAE,IAAI,EAAE,gCAAgC,EAAE,IAAI,EAAE,cAAc,EAAE,CAAA;AACtE,CAAC;AAED,4FAA4F;AAC5F,MAAM,UAAU,iBAAiB,CAAC,QAAuB;IACvD,OAAO,QAAQ,KAAK,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,aAAa,CAAA;AAC3D,CAAC;AAED;;;;;GAKG;AACH,SAAS,OAAO,CAAC,KAAkC;IACjD,OAAO,KAAK,CAAC,QAAQ,KAAK,OAAO,CAAA;AACnC,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAClC,MAA8C;IAE9C,OAAO,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAA;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,MAAoB;IAEpB,OAAO,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAA;AAClF,CAAC","sourcesContent":["import { CircleAlert, TriangleAlert } from 'lucide-react'\n\n/**\n * EC-507 — the studio's ONE issue-severity treatment.\n *\n * Three surfaces render a finding by severity: the document Issues pane\n * (`components/cms/issues-pane.tsx`, EC-283), the shell's issues tray\n * (`components/notifications/notice-center.tsx`, EC-690 — it was the section-rail foot,\n * EC-493) and the AI proposal card\n * (`components/ai/proposal-issues.tsx`, EC-496). They agreed on the treatment but\n * not on where it lived: EC-493 extracted `navIssueSeverityStyle`/`navIssueSeverityIcon`\n * beside the rail, EC-496 imported those `nav`-prefixed cores into a surface with\n * nothing to do with navigation, and the original stayed inlined in the pane — three\n * expressions of one rule, and no obvious home for the fourth surface to import from.\n * This module is that home. The `nav*` names are retired.\n *\n * Deliberately NOT here: the label/copy vocabulary. `cmsIssueLabel`, the message\n * catalog and `issueBodyDescriptor` are the EC-192 persona boundary — a separate\n * concern that happens to be rendered by the same cards. Presentation and copy stay\n * apart.\n *\n * Also not here: the `data-*` attributes each surface emits\n * (`data-nav-issue-severity`, `data-proposal-issue-severity`). Those are per-surface\n * test/inspection hooks, not part of the shared rule.\n */\n\n/**\n * The two values every contributing producer publishes — `CmsValidationIssue.severity`,\n * `CustomizationIssue.severity` and the project-graph `ValidationIssue.severity` are all\n * `'error' | 'warning'`. A surface carries the field across rather than re-deriving a\n * local notion of \"bad\".\n */\nexport type IssueSeverity = 'error' | 'warning'\n\n/**\n * The per-severity palette, as a pure mapping onto the shared token palette —\n * unit-testable without a DOM (the `statusDotToneClass` pattern).\n *\n * The values are the document Issues pane's, which had them first: destructive\n * border/fill/text for an error, warning border/fill/text for a warning. A surface\n * applies `card` to the finding's container and `text` to at least its glyph; whether\n * the tint also carries to the body copy is the surface's call (the pane keeps its\n * body `muted`, the rail and the proposal card tint the whole row), because that is\n * layout, not severity.\n */\nexport function issueSeverityStyle(severity: IssueSeverity): {\n /** Border + fill for the finding's card. */\n card: string\n /** Icon + message tone. */\n text: string\n} {\n return severity === 'error'\n ? { card: 'border-destructive/30 bg-destructive/5', text: 'text-destructive' }\n : { card: 'border-warning/30 bg-warning/5', text: 'text-warning' }\n}\n\n/** The glyph for a severity — `CircleAlert` for an error, `TriangleAlert` for a warning. */\nexport function issueSeverityIcon(severity: IssueSeverity) {\n return severity === 'error' ? CircleAlert : TriangleAlert\n}\n\n/**\n * **The rank rule: `error` outranks `warning`.** Stated once here because two\n * surfaces express it differently — the collapsed rail shows one aggregate glyph\n * ({@link highestIssueSeverity}), the proposal card shows every finding and sorts\n * them ({@link orderBySeverity}) — and a rule expressed twice drifts.\n */\nfunction isError(issue: { severity: IssueSeverity }): boolean {\n return issue.severity === 'error'\n}\n\n/**\n * The severity ONE aggregate glyph should report for a mixed list: **the highest\n * present**.\n *\n * An icons-only rail may over-state the mildest finding, but it must never\n * under-report a hard error down to an advisory — the reader has no second signal\n * there, and expanding immediately corrects any over-statement.\n *\n * An empty list has no severity to report; callers do not render the widget at all\n * in that case, and `'warning'` is the conservative answer for one that does.\n */\nexport function highestIssueSeverity(\n issues: readonly { severity: IssueSeverity }[],\n): IssueSeverity {\n return issues.some(isError) ? 'error' : 'warning'\n}\n\n/**\n * The same rank as ordering: **errors first**, warnings after, stable within each\n * severity.\n *\n * For a surface that shows every finding rather than one glyph. Where the card's\n * primary action commits the change and the reader scans top-down, the finding that\n * should give pause must not sit below one that should merely inform.\n */\nexport function orderBySeverity<T extends { severity: IssueSeverity }>(\n issues: readonly T[],\n): T[] {\n return [...issues.filter(isError), ...issues.filter((issue) => !isError(issue))]\n}\n"]}
@@ -1,22 +1,28 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import type { IssueSeverity } from './issue-severity.js';
3
- import type { NavIssueItem } from './nav-issues.js';
3
+ import type { Finding, NoticeLocation } from './notifications/notice-model.js';
4
4
  /**
5
- * EC-616 — the rail foot's CONTRIBUTION seam (`useNavIssues`, ruling § 6b item 1).
5
+ * EC-616 — the studio's CONTRIBUTION seam for findings (`useNavIssues`, ruling § 6b item 1);
6
+ * since EC-690 the registry of the ONE notification-and-issue surface: everything it holds
7
+ * is listed in the shell header's issues tray (the badge), beside the engine's own findings.
6
8
  *
7
- * The issues foot (`SectionNav`) is on every studio surface, so it is where a finding the
8
- * developer must not miss belongs — but its sources were a fixed list (config, nav,
9
- * renderers, reuse, routes, orphans), and a custom view or an addon had no way in. Now any
10
- * component under the studio shell publishes findings with {@link useNavIssues}; the foot
11
- * renders them beside its own. While the publishing component is mounted its findings
12
- * show; on unmount they go. A rail item that must ALWAYS report (not only while its view
13
- * is open) declares `useIssues` on its view (`nav.view({ useIssues })` /
14
- * `nav.rail(kind, { useIssues })`) — the rail mounts that hook for it.
9
+ * Any component under the studio shell publishes findings with {@link useNavIssues}. While
10
+ * the publishing component is mounted its findings are listed; on unmount they go. A rail
11
+ * item that must ALWAYS report (not only while its view is open) declares `useIssues` on its
12
+ * view (`nav.view({ useIssues })` / `nav.rail(kind, { useIssues })`) — the shell mounts that
13
+ * hook for it.
15
14
  *
16
15
  * A finding is plain data: a stable `key` (unique within its source), the localized
17
- * `message`, and a `severity` (`'error' | 'warning'`, default `'warning'`).
16
+ * `message`, a `severity` (`'error' | 'warning'`, default `'warning'`), and since EC-690 an
17
+ * optional short `title` and a `location` (where the tray's "Go to" leads, or the file it names).
18
+ *
19
+ * EC-690 (critic point 1): every publisher has its OWN identity, assigned by the registry
20
+ * (`useId`), so two mounted publishers that name the same source both stay listed — one can
21
+ * never overwrite or retract the other's findings. The engine's sources (`config`, `routes`,
22
+ * …, and the `engine:` prefix) are reserved: a contributor using one is still listed, and the
23
+ * misuse is reported as a finding of its own.
18
24
  */
19
- /** One finding a view or addon puts in the rail foot. */
25
+ /** One finding a view or addon publishes. */
20
26
  export interface NavIssue {
21
27
  /** Stable within its source (re-publishing the same key replaces, never duplicates). */
22
28
  key: string;
@@ -24,30 +30,39 @@ export interface NavIssue {
24
30
  message: string;
25
31
  /** Default `'warning'`. */
26
32
  severity?: IssueSeverity;
33
+ /** EC-690: a short localized title the tray leads the row with (e.g. "Invalid redirect"). */
34
+ title?: string;
35
+ /** EC-690: where the finding is — the tray's "Go to" (or the file it names). */
36
+ location?: NoticeLocation;
27
37
  }
28
38
  type Listener = () => void;
29
- /** The shell-scoped store of contributed findings, keyed by source. */
39
+ /** The shell-scoped store of contributed findings, keyed by PUBLISHER. */
30
40
  export declare class NavIssuesRegistry {
31
- private readonly bySource;
41
+ private readonly byPublisher;
32
42
  private readonly listeners;
33
43
  private snapshot;
34
44
  subscribe: (listener: Listener) => (() => void);
35
- getSnapshot: () => readonly NavIssueItem[];
36
- /** Publish (or replace) one source's findings; an empty list removes the source. */
37
- publish(source: string, issues: readonly NavIssue[]): void;
38
- retract(source: string): void;
45
+ getSnapshot: () => readonly Finding[];
46
+ /**
47
+ * Publish (or replace) ONE publisher's findings under `source`; an empty list removes the
48
+ * publisher. `publisher` is the registry-assigned identity ({@link useNavIssues} passes its
49
+ * `useId`), so another publisher naming the same source is untouched.
50
+ */
51
+ publish(publisher: string, source: string, issues: readonly NavIssue[]): void;
52
+ retract(publisher: string): void;
39
53
  private recompute;
40
54
  }
41
- /** Mounted once by the studio project screen, around the shell (rail + content). */
55
+ /** Mounted once by the studio project screen, around the shell (header, rail + content). */
42
56
  export declare function NavIssuesProvider({ children }: {
43
57
  children: ReactNode;
44
58
  }): import("react").JSX.Element;
45
59
  /**
46
- * Publish findings to the rail foot while this component is mounted. `source` names the
47
- * publisher (`'forms'`, `'analytics:beacon'`); pass the same `issues` array identity
48
- * between renders (memoize it) or the foot re-renders on every render.
60
+ * Publish findings to the shell's issues tray while this component is mounted. `source`
61
+ * names the publisher's kind (`'forms'`, `'analytics:beacon'`) — it groups and orders, it is
62
+ * not an address: each mounted call is its own publisher. Pass the same `issues` array
63
+ * identity between renders (memoize it) or the tray re-renders on every render.
49
64
  */
50
65
  export declare function useNavIssues(source: string, issues: readonly NavIssue[]): void;
51
- /** The contributed findings, for the rail foot. */
52
- export declare function useContributedNavIssues(): readonly NavIssueItem[];
66
+ /** The contributed findings, for the issues tray. */
67
+ export declare function useContributedNavIssues(): readonly Finding[];
53
68
  export {};