@elytracms/studio 0.0.97 → 0.0.99

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 (107) hide show
  1. package/convex/assets.ts +16 -2
  2. package/convex/ingest-attempt.test.ts +161 -0
  3. package/convex/ingest.test.ts +269 -16
  4. package/convex/ingest.ts +238 -47
  5. package/convex/path-segment-refusal.test.ts +201 -0
  6. package/convex/prototype-key.test.ts +92 -0
  7. package/convex/records-column-transform.convex.test.ts +147 -0
  8. package/convex/records-zod-refinements.convex.test.ts +94 -0
  9. package/convex/schema.ts +1 -1
  10. package/dist/addon.d.ts +5 -3
  11. package/dist/addon.js +4 -3
  12. package/dist/addon.js.map +1 -1
  13. package/dist/components/app-views/default-modules.d.ts +10 -3
  14. package/dist/components/app-views/default-modules.js +5 -1
  15. package/dist/components/app-views/default-modules.js.map +1 -1
  16. package/dist/components/app-views/module.d.ts +15 -8
  17. package/dist/components/app-views/module.js.map +1 -1
  18. package/dist/components/builder/reuse-context.d.ts +29 -1
  19. package/dist/components/builder/reuse-context.js +58 -12
  20. package/dist/components/builder/reuse-context.js.map +1 -1
  21. package/dist/components/cms/cms-store.d.ts +5 -6
  22. package/dist/components/cms/cms-store.js +12 -24
  23. package/dist/components/cms/cms-store.js.map +1 -1
  24. package/dist/components/cms/field-control.d.ts +12 -1
  25. package/dist/components/cms/field-control.js +31 -6
  26. package/dist/components/cms/field-control.js.map +1 -1
  27. package/dist/components/cms/import-batch.d.ts +23 -0
  28. package/dist/components/cms/import-batch.js +43 -0
  29. package/dist/components/cms/import-batch.js.map +1 -0
  30. package/dist/components/cms/rich-text/editor.js +13 -10
  31. package/dist/components/cms/rich-text/editor.js.map +1 -1
  32. package/dist/components/cms/rich-text/value.d.ts +9 -0
  33. package/dist/components/cms/rich-text/value.js +13 -0
  34. package/dist/components/cms/rich-text/value.js.map +1 -1
  35. package/dist/components/cms/use-document-draft.d.ts +39 -3
  36. package/dist/components/cms/use-document-draft.js +46 -16
  37. package/dist/components/cms/use-document-draft.js.map +1 -1
  38. package/dist/components/cms/use-publish-controller.d.ts +7 -0
  39. package/dist/components/cms/use-publish-controller.js +17 -3
  40. package/dist/components/cms/use-publish-controller.js.map +1 -1
  41. package/dist/components/cms/versions-pane.js +8 -5
  42. package/dist/components/cms/versions-pane.js.map +1 -1
  43. package/dist/components/delivery-health-banner.d.ts +34 -16
  44. package/dist/components/delivery-health-banner.js +59 -27
  45. package/dist/components/delivery-health-banner.js.map +1 -1
  46. package/dist/components/issue-severity.d.ts +3 -2
  47. package/dist/components/issue-severity.js.map +1 -1
  48. package/dist/components/nav-issues-registry.d.ts +39 -24
  49. package/dist/components/nav-issues-registry.js +50 -21
  50. package/dist/components/nav-issues-registry.js.map +1 -1
  51. package/dist/components/nav-issues.d.ts +23 -50
  52. package/dist/components/nav-issues.js +31 -59
  53. package/dist/components/nav-issues.js.map +1 -1
  54. package/dist/components/notifications/notice-center.d.ts +61 -0
  55. package/dist/components/notifications/notice-center.js +119 -0
  56. package/dist/components/notifications/notice-center.js.map +1 -0
  57. package/dist/components/notifications/notice-model.d.ts +137 -0
  58. package/dist/components/notifications/notice-model.js +107 -0
  59. package/dist/components/notifications/notice-model.js.map +1 -0
  60. package/dist/components/notifications/outcome-notice.d.ts +16 -0
  61. package/dist/components/notifications/outcome-notice.js +34 -0
  62. package/dist/components/notifications/outcome-notice.js.map +1 -0
  63. package/dist/components/notifications/shell-findings.d.ts +36 -0
  64. package/dist/components/notifications/shell-findings.js +140 -0
  65. package/dist/components/notifications/shell-findings.js.map +1 -0
  66. package/dist/components/rail-view-source.js +5 -0
  67. package/dist/components/rail-view-source.js.map +1 -1
  68. package/dist/components/records/record-lens.js.map +1 -1
  69. package/dist/components/records/record-rail.d.ts +10 -2
  70. package/dist/components/records/record-rail.js +74 -4
  71. package/dist/components/records/record-rail.js.map +1 -1
  72. package/dist/components/section-nav.d.ts +15 -3
  73. package/dist/components/section-nav.js +39 -147
  74. package/dist/components/section-nav.js.map +1 -1
  75. package/dist/components/studio-project-screen.js.map +1 -1
  76. package/dist/components/studio-shell.js +8 -3
  77. package/dist/components/studio-shell.js.map +1 -1
  78. package/dist/components/use-narrow-viewport.d.ts +11 -0
  79. package/dist/components/use-narrow-viewport.js +25 -0
  80. package/dist/components/use-narrow-viewport.js.map +1 -0
  81. package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
  82. package/dist/components/users/role-policy-drift-issues.js +17 -8
  83. package/dist/components/users/role-policy-drift-issues.js.map +1 -1
  84. package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
  85. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  86. package/dist/lib/engine-version.d.ts +1 -1
  87. package/dist/lib/engine-version.js +1 -1
  88. package/dist/lib/engine-version.js.map +1 -1
  89. package/dist/lib/i18n/messages.de.js +24 -3
  90. package/dist/lib/i18n/messages.de.js.map +1 -1
  91. package/dist/lib/i18n/messages.en.d.ts +22 -2
  92. package/dist/lib/i18n/messages.en.js +27 -4
  93. package/dist/lib/i18n/messages.en.js.map +1 -1
  94. package/dist/lib/notices/toast-store.d.ts +61 -0
  95. package/dist/lib/notices/toast-store.js +116 -0
  96. package/dist/lib/notices/toast-store.js.map +1 -0
  97. package/dist/lib/operations/cms-persistence.d.ts +32 -7
  98. package/dist/lib/operations/cms-persistence.js +511 -71
  99. package/dist/lib/operations/cms-persistence.js.map +1 -1
  100. package/dist/lib/persistence/convex-adapter.js +61 -30
  101. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  102. package/dist/lib/persona/labels.js +1 -0
  103. package/dist/lib/persona/labels.js.map +1 -1
  104. package/dist/ui/toast.d.ts +24 -16
  105. package/dist/ui/toast.js +23 -32
  106. package/dist/ui/toast.js.map +1 -1
  107. package/package.json +4 -3
@@ -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 {};
@@ -1,8 +1,10 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useMemo, useSyncExternalStore } from 'react';
3
- /** The shell-scoped store of contributed findings, keyed by source. */
2
+ import { createContext, useContext, useEffect, useId, useMemo, useSyncExternalStore } from 'react';
3
+ import { translate } from '../lib/i18n/index.js';
4
+ import { isReservedFindingSource } from './notifications/notice-model.js';
5
+ /** The shell-scoped store of contributed findings, keyed by PUBLISHER. */
4
6
  export class NavIssuesRegistry {
5
- bySource = new Map();
7
+ byPublisher = new Map();
6
8
  listeners = new Set();
7
9
  snapshot = [];
8
10
  subscribe = (listener) => {
@@ -12,23 +14,48 @@ export class NavIssuesRegistry {
12
14
  };
13
15
  };
14
16
  getSnapshot = () => this.snapshot;
15
- /** Publish (or replace) one source's findings; an empty list removes the source. */
16
- publish(source, issues) {
17
- if (issues.length === 0)
18
- this.bySource.delete(source);
19
- else
20
- this.bySource.set(source, issues);
17
+ /**
18
+ * Publish (or replace) ONE publisher's findings under `source`; an empty list removes the
19
+ * publisher. `publisher` is the registry-assigned identity ({@link useNavIssues} passes its
20
+ * `useId`), so another publisher naming the same source is untouched.
21
+ */
22
+ publish(publisher, source, issues) {
23
+ if (issues.length === 0) {
24
+ if (!this.byPublisher.delete(publisher))
25
+ return;
26
+ }
27
+ else {
28
+ this.byPublisher.set(publisher, { source, issues });
29
+ }
21
30
  this.recompute();
22
31
  }
23
- retract(source) {
24
- if (this.bySource.delete(source))
32
+ retract(publisher) {
33
+ if (this.byPublisher.delete(publisher))
25
34
  this.recompute();
26
35
  }
27
36
  recompute() {
28
37
  const items = [];
29
- for (const [source, issues] of this.bySource) {
38
+ for (const [publisher, { source, issues }] of this.byPublisher) {
30
39
  for (const issue of issues) {
31
- items.push({ key: `contributed:${source}:${issue.key}`, message: issue.message, severity: issue.severity ?? 'warning' });
40
+ items.push({
41
+ id: `${publisher}:${issue.key}`,
42
+ source,
43
+ key: issue.key,
44
+ message: issue.message,
45
+ severity: issue.severity ?? 'warning',
46
+ ...(issue.title !== undefined ? { title: issue.title } : {}),
47
+ ...(issue.location !== undefined ? { location: issue.location } : {}),
48
+ });
49
+ }
50
+ if (isReservedFindingSource(source)) {
51
+ items.push({
52
+ id: `${publisher}:reserved-source`,
53
+ source: 'engine:registry',
54
+ key: `reserved-source:${source}`,
55
+ message: translate('shell.notices.reservedSource', { source }),
56
+ severity: 'error',
57
+ location: { kind: 'config', declaredIn: 'studio' },
58
+ });
32
59
  }
33
60
  }
34
61
  this.snapshot = items;
@@ -37,7 +64,7 @@ export class NavIssuesRegistry {
37
64
  }
38
65
  }
39
66
  const NavIssuesContext = createContext(null);
40
- /** Mounted once by the studio project screen, around the shell (rail + content). */
67
+ /** Mounted once by the studio project screen, around the shell (header, rail + content). */
41
68
  export function NavIssuesProvider({ children }) {
42
69
  const registry = useMemo(() => new NavIssuesRegistry(), []);
43
70
  return _jsx(NavIssuesContext.Provider, { value: registry, children: children });
@@ -50,18 +77,20 @@ function useRegistry(caller) {
50
77
  return registry;
51
78
  }
52
79
  /**
53
- * Publish findings to the rail foot while this component is mounted. `source` names the
54
- * publisher (`'forms'`, `'analytics:beacon'`); pass the same `issues` array identity
55
- * between renders (memoize it) or the foot re-renders on every render.
80
+ * Publish findings to the shell's issues tray while this component is mounted. `source`
81
+ * names the publisher's kind (`'forms'`, `'analytics:beacon'`) — it groups and orders, it is
82
+ * not an address: each mounted call is its own publisher. Pass the same `issues` array
83
+ * identity between renders (memoize it) or the tray re-renders on every render.
56
84
  */
57
85
  export function useNavIssues(source, issues) {
58
86
  const registry = useRegistry('useNavIssues');
87
+ const publisher = useId();
59
88
  useEffect(() => {
60
- registry.publish(source, issues);
61
- }, [registry, source, issues]);
62
- useEffect(() => () => registry.retract(source), [registry, source]);
89
+ registry.publish(publisher, source, issues);
90
+ }, [registry, publisher, source, issues]);
91
+ useEffect(() => () => registry.retract(publisher), [registry, publisher]);
63
92
  }
64
- /** The contributed findings, for the rail foot. */
93
+ /** The contributed findings, for the issues tray. */
65
94
  export function useContributedNavIssues() {
66
95
  const registry = useRegistry('useContributedNavIssues');
67
96
  return useSyncExternalStore(registry.subscribe, registry.getSnapshot, registry.getSnapshot);