@elytracms/studio 0.0.42 → 0.0.44

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 (229) hide show
  1. package/dist/components/app-views/studio-config.d.ts +12 -0
  2. package/dist/components/app-views/studio-config.js.map +1 -1
  3. package/dist/components/assets/asset-detail.js +1 -1
  4. package/dist/components/assets/asset-detail.js.map +1 -1
  5. package/dist/components/assets/assets-collection-view-source.d.ts +13 -1
  6. package/dist/components/assets/assets-collection-view-source.js +42 -51
  7. package/dist/components/assets/assets-collection-view-source.js.map +1 -1
  8. package/dist/components/assets/assets-module.js +19 -6
  9. package/dist/components/assets/assets-module.js.map +1 -1
  10. package/dist/components/assets/assets-store.d.ts +2 -1
  11. package/dist/components/assets/assets-store.js +4 -8
  12. package/dist/components/assets/assets-store.js.map +1 -1
  13. package/dist/components/assets/assets-view-preference.d.ts +0 -8
  14. package/dist/components/assets/assets-view-preference.js +12 -13
  15. package/dist/components/assets/assets-view-preference.js.map +1 -1
  16. package/dist/components/auth-context.d.ts +2 -1
  17. package/dist/components/auth-context.js +6 -8
  18. package/dist/components/auth-context.js.map +1 -1
  19. package/dist/components/builder/builder-store.d.ts +2 -2
  20. package/dist/components/builder/builder-store.js +4 -8
  21. package/dist/components/builder/builder-store.js.map +1 -1
  22. package/dist/components/cms/bulk-actions.js +2 -1
  23. package/dist/components/cms/bulk-actions.js.map +1 -1
  24. package/dist/components/cms/cell-edit.js +2 -1
  25. package/dist/components/cms/cell-edit.js.map +1 -1
  26. package/dist/components/cms/cms-composition-validators.d.ts +25 -0
  27. package/dist/components/cms/cms-composition-validators.js +75 -0
  28. package/dist/components/cms/cms-composition-validators.js.map +1 -0
  29. package/dist/components/cms/cms-labels.d.ts +15 -0
  30. package/dist/components/cms/cms-labels.js +27 -0
  31. package/dist/components/cms/cms-labels.js.map +1 -0
  32. package/dist/components/cms/cms-store.d.ts +23 -10
  33. package/dist/components/cms/cms-store.js +87 -165
  34. package/dist/components/cms/cms-store.js.map +1 -1
  35. package/dist/components/cms/collection-table.js +1 -1
  36. package/dist/components/cms/collection-table.js.map +1 -1
  37. package/dist/components/cms/detail-editor.js +111 -348
  38. package/dist/components/cms/detail-editor.js.map +1 -1
  39. package/dist/components/cms/discard-draft.d.ts +18 -0
  40. package/dist/components/cms/discard-draft.js +18 -0
  41. package/dist/components/cms/discard-draft.js.map +1 -1
  42. package/dist/components/cms/document-editor-context.d.ts +2 -1
  43. package/dist/components/cms/document-editor-context.js +4 -9
  44. package/dist/components/cms/document-editor-context.js.map +1 -1
  45. package/dist/components/cms/document-ref-utils.d.ts +33 -0
  46. package/dist/components/cms/document-ref-utils.js +47 -0
  47. package/dist/components/cms/document-ref-utils.js.map +1 -0
  48. package/dist/components/cms/document-staging.d.ts +8 -3
  49. package/dist/components/cms/document-staging.js +81 -21
  50. package/dist/components/cms/document-staging.js.map +1 -1
  51. package/dist/components/cms/field-control.js +1 -1
  52. package/dist/components/cms/field-control.js.map +1 -1
  53. package/dist/components/cms/issues-pane.js +1 -1
  54. package/dist/components/cms/issues-pane.js.map +1 -1
  55. package/dist/components/cms/rich-text/block-commands.d.ts +36 -0
  56. package/dist/components/cms/rich-text/block-commands.js +47 -0
  57. package/dist/components/cms/rich-text/block-commands.js.map +1 -0
  58. package/dist/components/cms/rich-text/slash-command.js +25 -8
  59. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  60. package/dist/components/cms/rich-text/toolbar.js +3 -2
  61. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  62. package/dist/components/cms/use-compare-selection.d.ts +27 -0
  63. package/dist/components/cms/use-compare-selection.js +29 -0
  64. package/dist/components/cms/use-compare-selection.js.map +1 -0
  65. package/dist/components/cms/use-discard-controller.d.ts +44 -0
  66. package/dist/components/cms/use-discard-controller.js +51 -0
  67. package/dist/components/cms/use-discard-controller.js.map +1 -0
  68. package/dist/components/cms/use-embed-inspector-host.d.ts +12 -0
  69. package/dist/components/cms/use-embed-inspector-host.js +17 -0
  70. package/dist/components/cms/use-embed-inspector-host.js.map +1 -0
  71. package/dist/components/cms/use-field-focus.d.ts +38 -0
  72. package/dist/components/cms/use-field-focus.js +70 -0
  73. package/dist/components/cms/use-field-focus.js.map +1 -0
  74. package/dist/components/cms/use-live-url.d.ts +41 -0
  75. package/dist/components/cms/use-live-url.js +55 -0
  76. package/dist/components/cms/use-live-url.js.map +1 -0
  77. package/dist/components/cms/use-publish-controller.d.ts +49 -0
  78. package/dist/components/cms/use-publish-controller.js +102 -0
  79. package/dist/components/cms/use-publish-controller.js.map +1 -0
  80. package/dist/components/cms/use-published-perspective.d.ts +67 -0
  81. package/dist/components/cms/use-published-perspective.js +67 -0
  82. package/dist/components/cms/use-published-perspective.js.map +1 -0
  83. package/dist/components/cms/use-restore-version.d.ts +57 -0
  84. package/dist/components/cms/use-restore-version.js +66 -0
  85. package/dist/components/cms/use-restore-version.js.map +1 -0
  86. package/dist/components/cms/use-save-mode.d.ts +35 -0
  87. package/dist/components/cms/use-save-mode.js +54 -0
  88. package/dist/components/cms/use-save-mode.js.map +1 -0
  89. package/dist/components/cms/version-compare.js +14 -17
  90. package/dist/components/cms/version-compare.js.map +1 -1
  91. package/dist/components/cms/versions-model.d.ts +31 -0
  92. package/dist/components/cms/versions-model.js +30 -0
  93. package/dist/components/cms/versions-model.js.map +1 -1
  94. package/dist/components/cms/versions-pane.js +21 -19
  95. package/dist/components/cms/versions-pane.js.map +1 -1
  96. package/dist/components/cms/view-bar.js +9 -6
  97. package/dist/components/cms/view-bar.js.map +1 -1
  98. package/dist/components/collab/collab-context.d.ts +2 -1
  99. package/dist/components/collab/collab-context.js +4 -8
  100. package/dist/components/collab/collab-context.js.map +1 -1
  101. package/dist/components/collection-view/collection-view-context.d.ts +9 -1
  102. package/dist/components/collection-view/collection-view-context.js +4 -9
  103. package/dist/components/collection-view/collection-view-context.js.map +1 -1
  104. package/dist/components/components/component-detail.js +2 -5
  105. package/dist/components/components/component-detail.js.map +1 -1
  106. package/dist/components/components/component-preview.js +8 -4
  107. package/dist/components/components/component-preview.js.map +1 -1
  108. package/dist/components/components/registry-browser.js +24 -5
  109. package/dist/components/components/registry-browser.js.map +1 -1
  110. package/dist/components/components/transfer-dialog.d.ts +21 -6
  111. package/dist/components/components/transfer-dialog.js +120 -31
  112. package/dist/components/components/transfer-dialog.js.map +1 -1
  113. package/dist/components/components/transfer.d.ts +74 -8
  114. package/dist/components/components/transfer.js +87 -2
  115. package/dist/components/components/transfer.js.map +1 -1
  116. package/dist/components/delivery-health-banner.d.ts +12 -6
  117. package/dist/components/delivery-health-banner.js +27 -29
  118. package/dist/components/delivery-health-banner.js.map +1 -1
  119. package/dist/components/dev-login-badge.d.ts +0 -2
  120. package/dist/components/dev-login-badge.js +1 -3
  121. package/dist/components/dev-login-badge.js.map +1 -1
  122. package/dist/components/playground-badge.d.ts +0 -2
  123. package/dist/components/playground-badge.js +0 -2
  124. package/dist/components/playground-badge.js.map +1 -1
  125. package/dist/components/project-list.js +2 -6
  126. package/dist/components/project-list.js.map +1 -1
  127. package/dist/components/project-session.d.ts +6 -2
  128. package/dist/components/project-session.js +32 -83
  129. package/dist/components/project-session.js.map +1 -1
  130. package/dist/components/project-switcher.js +2 -6
  131. package/dist/components/project-switcher.js.map +1 -1
  132. package/dist/components/rail-store-status.d.ts +7 -5
  133. package/dist/components/rail-store-status.js +12 -9
  134. package/dist/components/rail-store-status.js.map +1 -1
  135. package/dist/components/rail-view-source.d.ts +111 -0
  136. package/dist/components/rail-view-source.js +139 -0
  137. package/dist/components/rail-view-source.js.map +1 -0
  138. package/dist/components/redirects/redirects-collection-view-source.d.ts +13 -1
  139. package/dist/components/redirects/redirects-collection-view-source.js +42 -70
  140. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  141. package/dist/components/redirects/redirects-store.d.ts +2 -1
  142. package/dist/components/redirects/redirects-store.js +4 -8
  143. package/dist/components/redirects/redirects-store.js.map +1 -1
  144. package/dist/components/section-state.d.ts +12 -3
  145. package/dist/components/section-state.js +8 -4
  146. package/dist/components/section-state.js.map +1 -1
  147. package/dist/components/studio-context.d.ts +2 -1
  148. package/dist/components/studio-context.js +4 -8
  149. package/dist/components/studio-context.js.map +1 -1
  150. package/dist/components/studio-shell.js +1 -1
  151. package/dist/components/studio-shell.js.map +1 -1
  152. package/dist/components/theme-context.d.ts +2 -1
  153. package/dist/components/theme-context.js +4 -8
  154. package/dist/components/theme-context.js.map +1 -1
  155. package/dist/components/use-hydration-phase.d.ts +65 -0
  156. package/dist/components/use-hydration-phase.js +64 -0
  157. package/dist/components/use-hydration-phase.js.map +1 -0
  158. package/dist/components/users/users-collection-view-source.d.ts +13 -1
  159. package/dist/components/users/users-collection-view-source.js +60 -79
  160. package/dist/components/users/users-collection-view-source.js.map +1 -1
  161. package/dist/components/users/users-store.d.ts +2 -1
  162. package/dist/components/users/users-store.js +4 -8
  163. package/dist/components/users/users-store.js.map +1 -1
  164. package/dist/components/workspace-projects.d.ts +14 -0
  165. package/dist/components/workspace-projects.js +21 -0
  166. package/dist/components/workspace-projects.js.map +1 -1
  167. package/dist/lib/ai/anthropic-provider.d.ts +29 -0
  168. package/dist/lib/ai/anthropic-provider.js +59 -0
  169. package/dist/lib/ai/anthropic-provider.js.map +1 -0
  170. package/dist/lib/ai/apply-content.d.ts +9 -0
  171. package/dist/lib/ai/apply-content.js +170 -0
  172. package/dist/lib/ai/apply-content.js.map +1 -0
  173. package/dist/lib/ai/config.d.ts +58 -0
  174. package/dist/lib/ai/config.js +134 -0
  175. package/dist/lib/ai/config.js.map +1 -0
  176. package/dist/lib/ai/fake-provider.d.ts +34 -0
  177. package/dist/lib/ai/fake-provider.js +31 -0
  178. package/dist/lib/ai/fake-provider.js.map +1 -0
  179. package/dist/lib/ai/index.d.ts +22 -0
  180. package/dist/lib/ai/index.js +23 -0
  181. package/dist/lib/ai/index.js.map +1 -0
  182. package/dist/lib/ai/knowledge.d.ts +39 -0
  183. package/dist/lib/ai/knowledge.js +60 -0
  184. package/dist/lib/ai/knowledge.js.map +1 -0
  185. package/dist/lib/ai/provenance.d.ts +20 -0
  186. package/dist/lib/ai/provenance.js +15 -0
  187. package/dist/lib/ai/provenance.js.map +1 -0
  188. package/dist/lib/ai/route.d.ts +24 -0
  189. package/dist/lib/ai/route.js +90 -0
  190. package/dist/lib/ai/route.js.map +1 -0
  191. package/dist/lib/ai/shape.d.ts +35 -0
  192. package/dist/lib/ai/shape.js +113 -0
  193. package/dist/lib/ai/shape.js.map +1 -0
  194. package/dist/lib/ai/types.d.ts +227 -0
  195. package/dist/lib/ai/types.js +2 -0
  196. package/dist/lib/ai/types.js.map +1 -0
  197. package/dist/lib/data/data-store.d.ts +2 -1
  198. package/dist/lib/data/data-store.js +4 -8
  199. package/dist/lib/data/data-store.js.map +1 -1
  200. package/dist/lib/delivery/instant-publish.d.ts +9 -13
  201. package/dist/lib/delivery/instant-publish.js +0 -7
  202. package/dist/lib/delivery/instant-publish.js.map +1 -1
  203. package/dist/lib/diff-view-preference.js +4 -4
  204. package/dist/lib/diff-view-preference.js.map +1 -1
  205. package/dist/lib/i18n/messages.en.js +5 -2
  206. package/dist/lib/i18n/messages.en.js.map +1 -1
  207. package/dist/lib/local-preference.d.ts +37 -0
  208. package/dist/lib/local-preference.js +36 -0
  209. package/dist/lib/local-preference.js.map +1 -1
  210. package/dist/lib/persistence/convex-adapter.d.ts +3 -1
  211. package/dist/lib/persistence/convex-adapter.js +20 -0
  212. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  213. package/dist/lib/rail.js +7 -6
  214. package/dist/lib/rail.js.map +1 -1
  215. package/dist/lib/required-context.d.ts +24 -0
  216. package/dist/lib/required-context.js +28 -0
  217. package/dist/lib/required-context.js.map +1 -0
  218. package/dist/lib/theme.js +11 -4
  219. package/dist/lib/theme.js.map +1 -1
  220. package/dist/lib/workspace/index.d.ts +9 -0
  221. package/dist/lib/workspace/index.js +11 -0
  222. package/dist/lib/workspace/index.js.map +1 -1
  223. package/dist/ui/index.d.ts +1 -0
  224. package/dist/ui/index.js +1 -0
  225. package/dist/ui/index.js.map +1 -1
  226. package/dist/ui/segmented-control.d.ts +46 -0
  227. package/dist/ui/segmented-control.js +13 -0
  228. package/dist/ui/segmented-control.js.map +1 -0
  229. package/package.json +8 -2
@@ -4,7 +4,7 @@ import { Button, Row, Stack, Text } from '@elytracms/studio/ui';
4
4
  import { cn } from '@elytracms/studio/lib/utils';
5
5
  import { cmsIssueLabel } from '@elytracms/studio/lib/persona/labels';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
- import { fieldLabel } from './cms-store';
7
+ import { fieldLabel } from './cms-labels';
8
8
  /**
9
9
  * EC-283 — the document-scoped validation surface.
10
10
  *
@@ -1 +1 @@
1
- {"version":3,"file":"issues-pane.js","sourceRoot":"","sources":["../../../src/components/cms/issues-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { CircleAlert, CircleCheck, TriangleAlert } from 'lucide-react'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { cmsIssueLabel } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { fieldLabel } from './cms-store'\n\n/**\n * EC-283 — the document-scoped validation surface.\n *\n * Lists every {@link CmsValidationIssue} for the open draft in the right\n * `InspectorSidebar` (the document-scoped equivalent of the amputated builder's\n * validation sidebar, EC-069). Field-level issues stay inline on the field too;\n * here they additionally list with a jump-to-field action. Document-level issues\n * that carry no `meta.field` (e.g. the EC-260 nesting-depth warning) have no\n * other home — this is theirs.\n *\n * Persona boundary (EC-192): rows show the plain-language `cmsIssueLabel`; the raw\n * `code` rides along in `title=` for engineers, never as visible copy.\n */\nexport function IssuesPane({\n issues,\n collection,\n onJumpToField,\n}: {\n issues: readonly CmsValidationIssue[]\n collection: CollectionDef\n onJumpToField: (field: string) => void\n}) {\n if (issues.length === 0) {\n return (\n <Stack gap=\"2\" align=\"center\" className=\"px-4 py-10 text-center\">\n <CircleCheck className=\"size-5 text-success\" aria-hidden />\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.issues.none')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.issues.satisfied')}\n </Text>\n </Stack>\n )\n }\n\n return (\n <Stack gap=\"1\" className=\"p-2\">\n {issues.map((issue, i) => (\n <IssueRow\n key={`${issue.code}:${issue.path.join('/')}:${i}`}\n issue={issue}\n collection={collection}\n onJumpToField={onJumpToField}\n />\n ))}\n </Stack>\n )\n}\n\nfunction IssueRow({\n issue,\n collection,\n onJumpToField,\n}: {\n issue: CmsValidationIssue\n collection: CollectionDef\n onJumpToField: (field: string) => void\n}) {\n const isError = issue.severity === 'error'\n const fieldName = typeof issue.meta?.field === 'string' ? issue.meta.field : undefined\n const fieldDef = fieldName ? collection.fields.find((f) => f.name === fieldName) : undefined\n const Icon = isError ? CircleAlert : TriangleAlert\n\n return (\n <Stack\n gap=\"1\"\n className={cn(\n 'rounded-md border px-2.5 py-2',\n isError ? 'border-destructive/30 bg-destructive/5' : 'border-warning/30 bg-warning/5',\n )}\n >\n <Row gap=\"2\" align=\"start\">\n <Icon\n className={cn('mt-0.5 size-3.5 shrink-0', isError ? 'text-destructive' : 'text-warning')}\n aria-hidden\n />\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" weight=\"semibold\" title={issue.code}>\n {cmsIssueLabel(issue.code)}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {issue.message}\n </Text>\n </Stack>\n </Row>\n {fieldName ? (\n <Row className=\"pl-[1.375rem]\">\n <Button\n size=\"sm\"\n variant=\"link\"\n className=\"h-auto p-0 text-xs\"\n onClick={() => onJumpToField(fieldName)}\n >\n {translate('cms.issues.goToField', { field: fieldDef ? fieldLabel(fieldDef) : fieldName })}\n </Button>\n </Row>\n ) : null}\n </Stack>\n )\n}\n"]}
1
+ {"version":3,"file":"issues-pane.js","sourceRoot":"","sources":["../../../src/components/cms/issues-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { CircleAlert, CircleCheck, TriangleAlert } from 'lucide-react'\nimport type { CmsValidationIssue, CollectionDef } from '@elytracms/core/cms-core'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { cmsIssueLabel } from '@elytracms/studio/lib/persona/labels'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { fieldLabel } from './cms-labels'\n\n/**\n * EC-283 — the document-scoped validation surface.\n *\n * Lists every {@link CmsValidationIssue} for the open draft in the right\n * `InspectorSidebar` (the document-scoped equivalent of the amputated builder's\n * validation sidebar, EC-069). Field-level issues stay inline on the field too;\n * here they additionally list with a jump-to-field action. Document-level issues\n * that carry no `meta.field` (e.g. the EC-260 nesting-depth warning) have no\n * other home — this is theirs.\n *\n * Persona boundary (EC-192): rows show the plain-language `cmsIssueLabel`; the raw\n * `code` rides along in `title=` for engineers, never as visible copy.\n */\nexport function IssuesPane({\n issues,\n collection,\n onJumpToField,\n}: {\n issues: readonly CmsValidationIssue[]\n collection: CollectionDef\n onJumpToField: (field: string) => void\n}) {\n if (issues.length === 0) {\n return (\n <Stack gap=\"2\" align=\"center\" className=\"px-4 py-10 text-center\">\n <CircleCheck className=\"size-5 text-success\" aria-hidden />\n <Text size=\"sm\" weight=\"medium\">\n {translate('cms.issues.none')}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.issues.satisfied')}\n </Text>\n </Stack>\n )\n }\n\n return (\n <Stack gap=\"1\" className=\"p-2\">\n {issues.map((issue, i) => (\n <IssueRow\n key={`${issue.code}:${issue.path.join('/')}:${i}`}\n issue={issue}\n collection={collection}\n onJumpToField={onJumpToField}\n />\n ))}\n </Stack>\n )\n}\n\nfunction IssueRow({\n issue,\n collection,\n onJumpToField,\n}: {\n issue: CmsValidationIssue\n collection: CollectionDef\n onJumpToField: (field: string) => void\n}) {\n const isError = issue.severity === 'error'\n const fieldName = typeof issue.meta?.field === 'string' ? issue.meta.field : undefined\n const fieldDef = fieldName ? collection.fields.find((f) => f.name === fieldName) : undefined\n const Icon = isError ? CircleAlert : TriangleAlert\n\n return (\n <Stack\n gap=\"1\"\n className={cn(\n 'rounded-md border px-2.5 py-2',\n isError ? 'border-destructive/30 bg-destructive/5' : 'border-warning/30 bg-warning/5',\n )}\n >\n <Row gap=\"2\" align=\"start\">\n <Icon\n className={cn('mt-0.5 size-3.5 shrink-0', isError ? 'text-destructive' : 'text-warning')}\n aria-hidden\n />\n <Stack gap=\"1\" className=\"min-w-0\">\n <Text size=\"xs\" weight=\"semibold\" title={issue.code}>\n {cmsIssueLabel(issue.code)}\n </Text>\n <Text size=\"xs\" tone=\"muted\">\n {issue.message}\n </Text>\n </Stack>\n </Row>\n {fieldName ? (\n <Row className=\"pl-[1.375rem]\">\n <Button\n size=\"sm\"\n variant=\"link\"\n className=\"h-auto p-0 text-xs\"\n onClick={() => onJumpToField(fieldName)}\n >\n {translate('cms.issues.goToField', { field: fieldDef ? fieldLabel(fieldDef) : fieldName })}\n </Button>\n </Row>\n ) : null}\n </Stack>\n )\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import type { Editor } from '@tiptap/react';
2
+ import type { LucideIcon } from 'lucide-react';
3
+ import type { ToolbarState } from './toolbar-state';
4
+ /**
5
+ * Shared block-command metadata (EC-328) — the five block operations
6
+ * `buildSlashItems` (slash-command.tsx) and `RichTextToolbar` (toolbar.tsx)
7
+ * independently paired with the same lucide icon and the same i18n key
8
+ * (confirmed intentional convergent design, EC-314), with no test enforcing
9
+ * icon/label parity between the two lists. Declaring the pairing ONCE here —
10
+ * consumed by both — makes that drift structurally impossible.
11
+ *
12
+ * Heading levels/paragraph and the image action are deliberately NOT in this
13
+ * table: the toolbar renders headings as literal "H1"/"H2"/"H3" text badges
14
+ * inside its style dropdown (no lucide icon in that slot), and the image
15
+ * action's `run` behavior genuinely differs between surfaces (the slash menu
16
+ * deletes the `/query` range before opening the picker; the toolbar button
17
+ * opens it directly) — folding either into this table would either regress
18
+ * the toolbar's existing badge style or conflate two different side-effect
19
+ * shapes behind one signature. The dynamic per-manifest "Components" group
20
+ * (0.0.41) is out of scope for the same reason: it isn't a static command.
21
+ */
22
+ export interface BlockCommandDescriptor {
23
+ key: string;
24
+ icon: LucideIcon;
25
+ /** i18n catalog key for the display label (no interpolation args needed). */
26
+ labelKey: string;
27
+ /** Extra fuzzy-match terms for the slash menu (the title itself is already matched). */
28
+ keywords: string;
29
+ /** Apply/toggle this command on an already-`.focus()`ed chain. */
30
+ chain: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>;
31
+ /** This command's boolean field in `ToolbarState`, or absent when it has no "active" concept (a divider). */
32
+ activeKey?: 'bulletList' | 'orderedList' | 'blockquote' | 'codeBlock';
33
+ }
34
+ export declare const BLOCK_COMMANDS: readonly BlockCommandDescriptor[];
35
+ /** The toolbar's `active` prop for one descriptor — `false` when it has no active concept. */
36
+ export declare function isBlockCommandActive(descriptor: BlockCommandDescriptor, state: ToolbarState): boolean;
@@ -0,0 +1,47 @@
1
+ import { List, ListOrdered, Minus, Quote, SquareCode } from 'lucide-react';
2
+ export const BLOCK_COMMANDS = [
3
+ {
4
+ key: 'bulletList',
5
+ icon: List,
6
+ labelKey: 'richText.bulletList',
7
+ keywords: 'bullet unordered list ul',
8
+ chain: (c) => c.toggleBulletList(),
9
+ activeKey: 'bulletList',
10
+ },
11
+ {
12
+ key: 'orderedList',
13
+ icon: ListOrdered,
14
+ labelKey: 'richText.orderedList',
15
+ keywords: 'ordered numbered list ol',
16
+ chain: (c) => c.toggleOrderedList(),
17
+ activeKey: 'orderedList',
18
+ },
19
+ {
20
+ key: 'blockquote',
21
+ icon: Quote,
22
+ labelKey: 'richText.blockquote',
23
+ keywords: 'quote blockquote',
24
+ chain: (c) => c.toggleBlockquote(),
25
+ activeKey: 'blockquote',
26
+ },
27
+ {
28
+ key: 'codeBlock',
29
+ icon: SquareCode,
30
+ labelKey: 'richText.codeBlock',
31
+ keywords: 'code block pre',
32
+ chain: (c) => c.toggleCodeBlock(),
33
+ activeKey: 'codeBlock',
34
+ },
35
+ {
36
+ key: 'divider',
37
+ icon: Minus,
38
+ labelKey: 'richText.horizontalRule',
39
+ keywords: 'divider rule hr separator',
40
+ chain: (c) => c.setHorizontalRule(),
41
+ },
42
+ ];
43
+ /** The toolbar's `active` prop for one descriptor — `false` when it has no active concept. */
44
+ export function isBlockCommandActive(descriptor, state) {
45
+ return descriptor.activeKey !== undefined ? state[descriptor.activeKey] : false;
46
+ }
47
+ //# sourceMappingURL=block-commands.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"block-commands.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/block-commands.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,cAAc,CAAA;AAmC1E,MAAM,CAAC,MAAM,cAAc,GAAsC;IAC/D;QACE,GAAG,EAAE,YAAY;QACjB,IAAI,EAAE,IAAI;QACV,QAAQ,EAAE,qBAAqB;QAC/B,QAAQ,EAAE,0BAA0B;QACpC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE;QAClC,SAAS,EAAE,YAAY;KACxB;IACD;QACE,GAAG,EAAE,aAAa;QAClB,IAAI,EAAE,WAAW;QACjB,QAAQ,EAAE,sBAAsB;QAChC,QAAQ,EAAE,0BAA0B;QACpC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB,EAAE;QACnC,SAAS,EAAE,aAAa;KACzB;IACD;QACE,GAAG,EAAE,YAAY;QACjB,IAAI,EAAE,KAAK;QACX,QAAQ,EAAE,qBAAqB;QAC/B,QAAQ,EAAE,kBAAkB;QAC5B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,EAAE;QAClC,SAAS,EAAE,YAAY;KACxB;IACD;QACE,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,UAAU;QAChB,QAAQ,EAAE,oBAAoB;QAC9B,QAAQ,EAAE,gBAAgB;QAC1B,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,eAAe,EAAE;QACjC,SAAS,EAAE,WAAW;KACvB;IACD;QACE,GAAG,EAAE,SAAS;QACd,IAAI,EAAE,KAAK;QACX,QAAQ,EAAE,yBAAyB;QACnC,QAAQ,EAAE,2BAA2B;QACrC,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,iBAAiB,EAAE;KACpC;CACO,CAAA;AAEV,8FAA8F;AAC9F,MAAM,UAAU,oBAAoB,CAAC,UAAkC,EAAE,KAAmB;IAC1F,OAAO,UAAU,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAA;AACjF,CAAC","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { List, ListOrdered, Minus, Quote, SquareCode } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport type { ToolbarState } from './toolbar-state'\n\n/**\n * Shared block-command metadata (EC-328) — the five block operations\n * `buildSlashItems` (slash-command.tsx) and `RichTextToolbar` (toolbar.tsx)\n * independently paired with the same lucide icon and the same i18n key\n * (confirmed intentional convergent design, EC-314), with no test enforcing\n * icon/label parity between the two lists. Declaring the pairing ONCE here —\n * consumed by both — makes that drift structurally impossible.\n *\n * Heading levels/paragraph and the image action are deliberately NOT in this\n * table: the toolbar renders headings as literal \"H1\"/\"H2\"/\"H3\" text badges\n * inside its style dropdown (no lucide icon in that slot), and the image\n * action's `run` behavior genuinely differs between surfaces (the slash menu\n * deletes the `/query` range before opening the picker; the toolbar button\n * opens it directly) — folding either into this table would either regress\n * the toolbar's existing badge style or conflate two different side-effect\n * shapes behind one signature. The dynamic per-manifest \"Components\" group\n * (0.0.41) is out of scope for the same reason: it isn't a static command.\n */\nexport interface BlockCommandDescriptor {\n key: string\n icon: LucideIcon\n /** i18n catalog key for the display label (no interpolation args needed). */\n labelKey: string\n /** Extra fuzzy-match terms for the slash menu (the title itself is already matched). */\n keywords: string\n /** Apply/toggle this command on an already-`.focus()`ed chain. */\n chain: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>\n /** This command's boolean field in `ToolbarState`, or absent when it has no \"active\" concept (a divider). */\n activeKey?: 'bulletList' | 'orderedList' | 'blockquote' | 'codeBlock'\n}\n\nexport const BLOCK_COMMANDS: readonly BlockCommandDescriptor[] = [\n {\n key: 'bulletList',\n icon: List,\n labelKey: 'richText.bulletList',\n keywords: 'bullet unordered list ul',\n chain: (c) => c.toggleBulletList(),\n activeKey: 'bulletList',\n },\n {\n key: 'orderedList',\n icon: ListOrdered,\n labelKey: 'richText.orderedList',\n keywords: 'ordered numbered list ol',\n chain: (c) => c.toggleOrderedList(),\n activeKey: 'orderedList',\n },\n {\n key: 'blockquote',\n icon: Quote,\n labelKey: 'richText.blockquote',\n keywords: 'quote blockquote',\n chain: (c) => c.toggleBlockquote(),\n activeKey: 'blockquote',\n },\n {\n key: 'codeBlock',\n icon: SquareCode,\n labelKey: 'richText.codeBlock',\n keywords: 'code block pre',\n chain: (c) => c.toggleCodeBlock(),\n activeKey: 'codeBlock',\n },\n {\n key: 'divider',\n icon: Minus,\n labelKey: 'richText.horizontalRule',\n keywords: 'divider rule hr separator',\n chain: (c) => c.setHorizontalRule(),\n },\n] as const\n\n/** The toolbar's `active` prop for one descriptor — `false` when it has no active concept. */\nexport function isBlockCommandActive(descriptor: BlockCommandDescriptor, state: ToolbarState): boolean {\n return descriptor.activeKey !== undefined ? state[descriptor.activeKey] : false\n}\n"]}
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { forwardRef, useEffect, useImperativeHandle, useState } from 'react';
2
+ import { forwardRef, useImperativeHandle, useState } from 'react';
3
3
  import { Extension, ReactRenderer } from '@tiptap/react';
4
4
  import Suggestion from '@tiptap/suggestion';
5
- import { Blocks, Heading1, Heading2, Heading3, ImagePlus, List, ListOrdered, Minus, Pilcrow, Quote, SquareCode, } from 'lucide-react';
5
+ import { Blocks, Heading1, Heading2, Heading3, ImagePlus, Pilcrow } from 'lucide-react';
6
6
  import { translate } from '@elytracms/studio/lib/i18n';
7
7
  import { componentDisplayName } from '@elytracms/studio/lib/persona/labels';
8
8
  import { cn } from '@elytracms/studio/lib/utils';
9
+ import { BLOCK_COMMANDS } from './block-commands';
9
10
  /** The command list, gated by which media actions + components the editor provided. */
10
11
  export function buildSlashItems(options) {
11
12
  const block = (run) => (editor, range) => run(editor.chain().focus().deleteRange(range)).run();
@@ -14,11 +15,15 @@ export function buildSlashItems(options) {
14
15
  { key: 'h1', title: translate('richText.heading', { level: 1 }), keywords: 'heading title h1', icon: Heading1, run: block((c) => c.toggleHeading({ level: 1 })) },
15
16
  { key: 'h2', title: translate('richText.heading', { level: 2 }), keywords: 'heading subtitle h2', icon: Heading2, run: block((c) => c.toggleHeading({ level: 2 })) },
16
17
  { key: 'h3', title: translate('richText.heading', { level: 3 }), keywords: 'heading h3', icon: Heading3, run: block((c) => c.toggleHeading({ level: 3 })) },
17
- { key: 'bulletList', title: translate('richText.bulletList'), keywords: 'bullet unordered list ul', icon: List, run: block((c) => c.toggleBulletList()) },
18
- { key: 'orderedList', title: translate('richText.orderedList'), keywords: 'ordered numbered list ol', icon: ListOrdered, run: block((c) => c.toggleOrderedList()) },
19
- { key: 'blockquote', title: translate('richText.blockquote'), keywords: 'quote blockquote', icon: Quote, run: block((c) => c.toggleBlockquote()) },
20
- { key: 'codeBlock', title: translate('richText.codeBlock'), keywords: 'code block pre', icon: SquareCode, run: block((c) => c.toggleCodeBlock()) },
21
- { key: 'divider', title: translate('richText.horizontalRule'), keywords: 'divider rule hr separator', icon: Minus, run: block((c) => c.setHorizontalRule()) },
18
+ // The shared table (EC-328) — bulletList/orderedList/blockquote/codeBlock/divider,
19
+ // the SAME descriptors `RichTextToolbar` derives its buttons from.
20
+ ...BLOCK_COMMANDS.map((cmd) => ({
21
+ key: cmd.key,
22
+ title: translate(cmd.labelKey),
23
+ keywords: cmd.keywords,
24
+ icon: cmd.icon,
25
+ run: block(cmd.chain),
26
+ })),
22
27
  ];
23
28
  if (options.onImage) {
24
29
  items.push({
@@ -58,7 +63,19 @@ export function filterSlashItems(items, query) {
58
63
  }
59
64
  const SlashMenu = forwardRef((props, ref) => {
60
65
  const [selected, setSelected] = useState(0);
61
- useEffect(() => setSelected(0), [props.items]);
66
+ // Reset the highlight DURING RENDER when `items` changes, not in a post-render
67
+ // Effect (EC-328 fix): `@tiptap/suggestion`'s `createSuggestionView.update()`
68
+ // dispatches a NEW `items` array on nearly every keystroke, so an
69
+ // effect-based reset landed one render late — a frame where `selected` was
70
+ // briefly stale/out-of-range against the freshly filtered list. Comparing
71
+ // against the previous reference here (React's documented "adjust state
72
+ // during render" idiom) resets it in the SAME render that receives the new
73
+ // `items`, before anything paints.
74
+ const [prevItems, setPrevItems] = useState(props.items);
75
+ if (props.items !== prevItems) {
76
+ setPrevItems(props.items);
77
+ setSelected(0);
78
+ }
62
79
  useImperativeHandle(ref, () => ({
63
80
  onKeyDown: ({ event }) => {
64
81
  const count = props.items.length;
@@ -1 +1 @@
1
- {"version":3,"file":"slash-command.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/slash-command.tsx"],"names":[],"mappings":"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","sourcesContent":["import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'\nimport type { ComponentType } from 'react'\nimport { Extension, ReactRenderer } from '@tiptap/react'\nimport type { Editor, Range } from '@tiptap/react'\nimport Suggestion from '@tiptap/suggestion'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport {\n Blocks,\n Heading1,\n Heading2,\n Heading3,\n ImagePlus,\n List,\n ListOrdered,\n Minus,\n Pilcrow,\n Quote,\n SquareCode,\n} from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * Notion-style slash menu (EC-314): typing `/` at the caret opens a filterable\n * command palette that inserts block-level content — headings, lists, quote, code,\n * divider — and, when available, an image or a registry component.\n *\n * 0.0.41: the embed path shows the permitted components **directly** as their own\n * items under a \"Components\" group (allow-gated exactly like the picker), rather than\n * a single \"Embed component\" item that opened a separate picker dialog — the\n * product-owner feedback that the extra step was unintuitive. Selecting a component\n * inserts it with manifest defaults and selects it, so the sidebar inspector opens.\n * Built on TipTap's `Suggestion` utility; the menu is a React overlay at the caret.\n */\n\n/** How the slash menu inserts a chosen component (owned by the editor). */\nexport interface SlashEmbedOptions {\n /** The permitted component manifests (already `allow`-gated by the field). */\n components: ComponentManifest[]\n /** Insert `manifest` at `range` (deletes the `/query`, inserts + selects). */\n insert: (editor: Editor, range: Range, manifest: ComponentManifest) => void\n}\n\nexport interface SlashCommandOptions {\n /** Open the asset picker (omitted ⇒ no `/image` item). */\n onImage?: () => void\n /** The embeddable components + inserter (omitted/empty ⇒ no component items). */\n embed?: SlashEmbedOptions\n}\n\nexport interface SlashItem {\n key: string\n title: string\n keywords: string\n icon: LucideIcon\n /** Optional group label — consecutive same-group items share a header. */\n group?: string\n run: (editor: Editor, range: Range) => void\n}\n\n/** The command list, gated by which media actions + components the editor provided. */\nexport function buildSlashItems(options: SlashCommandOptions): SlashItem[] {\n const block = (run: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>) =>\n (editor: Editor, range: Range) => run(editor.chain().focus().deleteRange(range)).run()\n\n const items: SlashItem[] = [\n { key: 'text', title: translate('richText.slash.text'), keywords: 'paragraph text plain', icon: Pilcrow, run: block((c) => c.setParagraph()) },\n { key: 'h1', title: translate('richText.heading', { level: 1 }), keywords: 'heading title h1', icon: Heading1, run: block((c) => c.toggleHeading({ level: 1 })) },\n { key: 'h2', title: translate('richText.heading', { level: 2 }), keywords: 'heading subtitle h2', icon: Heading2, run: block((c) => c.toggleHeading({ level: 2 })) },\n { key: 'h3', title: translate('richText.heading', { level: 3 }), keywords: 'heading h3', icon: Heading3, run: block((c) => c.toggleHeading({ level: 3 })) },\n { key: 'bulletList', title: translate('richText.bulletList'), keywords: 'bullet unordered list ul', icon: List, run: block((c) => c.toggleBulletList()) },\n { key: 'orderedList', title: translate('richText.orderedList'), keywords: 'ordered numbered list ol', icon: ListOrdered, run: block((c) => c.toggleOrderedList()) },\n { key: 'blockquote', title: translate('richText.blockquote'), keywords: 'quote blockquote', icon: Quote, run: block((c) => c.toggleBlockquote()) },\n { key: 'codeBlock', title: translate('richText.codeBlock'), keywords: 'code block pre', icon: SquareCode, run: block((c) => c.toggleCodeBlock()) },\n { key: 'divider', title: translate('richText.horizontalRule'), keywords: 'divider rule hr separator', icon: Minus, run: block((c) => c.setHorizontalRule()) },\n ]\n if (options.onImage) {\n items.push({\n key: 'image',\n title: translate('richText.image.add'),\n keywords: 'image picture photo media',\n icon: ImagePlus,\n run: (editor, range) => {\n editor.chain().focus().deleteRange(range).run()\n options.onImage?.()\n },\n })\n }\n const embed = options.embed\n if (embed) {\n const group = translate('richText.slash.componentsGroup')\n for (const manifest of embed.components) {\n const title = componentDisplayName(manifest.id, manifest)\n items.push({\n key: `embed:${manifest.id}`,\n title,\n keywords: `${title} ${manifest.id} component embed block`.toLowerCase(),\n icon: Blocks,\n group,\n run: (editor, range) => embed.insert(editor, range, manifest),\n })\n }\n }\n return items\n}\n\n/** Filter items by a typed query (title or keyword substring) — empty query passes all. */\nexport function filterSlashItems(items: SlashItem[], query: string): SlashItem[] {\n const q = query.trim().toLowerCase()\n if (!q) return items\n return items.filter((item) => item.title.toLowerCase().includes(q) || item.keywords.includes(q))\n}\n\n/** Imperative handle the suggestion render loop calls for keyboard navigation. */\ninterface SlashMenuHandle {\n onKeyDown: (props: SuggestionKeyDownProps) => boolean\n}\n\nconst SlashMenu = forwardRef<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>>((props, ref) => {\n const [selected, setSelected] = useState(0)\n useEffect(() => setSelected(0), [props.items])\n\n useImperativeHandle(ref, () => ({\n onKeyDown: ({ event }) => {\n const count = props.items.length\n if (count === 0) return false\n if (event.key === 'ArrowUp') {\n setSelected((s) => (s + count - 1) % count)\n return true\n }\n if (event.key === 'ArrowDown') {\n setSelected((s) => (s + 1) % count)\n return true\n }\n if (event.key === 'Enter') {\n const item = props.items[selected]\n if (item) props.command(item)\n return true\n }\n return false\n },\n }))\n\n if (props.items.length === 0) {\n return (\n <div className=\"ec-rte-slash\">\n <span className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {translate('richText.slash.empty')}\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"ec-rte-slash\" role=\"menu\">\n {props.items.map((item, i) => {\n const Icon = item.icon\n // A group header precedes the first item whose group differs from its\n // predecessor (the block-formatting items are ungrouped; components share one).\n const showHeader = item.group != null && item.group !== props.items[i - 1]?.group\n return (\n <div key={item.key}>\n {showHeader ? (\n <span className=\"block px-2 pt-2 pb-1 text-[0.6875rem] font-medium uppercase tracking-wide text-muted-foreground\">\n {item.group}\n </span>\n ) : null}\n <button\n type=\"button\"\n role=\"menuitem\"\n onMouseEnter={() => setSelected(i)}\n onMouseDown={(e) => {\n e.preventDefault()\n props.command(item)\n }}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm outline-none',\n i === selected ? 'bg-muted' : 'hover:bg-muted/60',\n )}\n >\n <Icon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{item.title}</span>\n </button>\n </div>\n )\n })}\n </div>\n )\n})\nSlashMenu.displayName = 'SlashMenu'\n\n/** Position a floating element under the caret, flipping above near the viewport edge. */\nfunction place(el: HTMLElement, rect: DOMRect | null | undefined): void {\n if (!rect) return\n const margin = 8\n const height = el.offsetHeight || 280\n const below = rect.bottom + 4\n const flip = below + height > window.innerHeight - margin && rect.top - height > margin\n el.style.position = 'fixed'\n el.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`\n el.style.top = flip ? `${rect.top - height - 4}px` : `${below}px`\n el.style.zIndex = '60'\n}\n\n/** The suggestion config (everything but the `editor`, which the extension injects). */\nfunction slashSuggestion(options: SlashCommandOptions): Omit<SuggestionOptions<SlashItem, SlashItem>, 'editor'> {\n return {\n char: '/',\n startOfLine: false,\n items: ({ query }) => filterSlashItems(buildSlashItems(options), query),\n command: ({ editor, range, props }) => props.run(editor, range),\n render: () => {\n let renderer: ReactRenderer<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>> | null = null\n let wrapper: HTMLDivElement | null = null\n\n return {\n onStart: (props) => {\n renderer = new ReactRenderer(SlashMenu as ComponentType<SuggestionProps<SlashItem, SlashItem>>, {\n props,\n editor: props.editor,\n })\n wrapper = document.createElement('div')\n wrapper.appendChild(renderer.element)\n document.body.appendChild(wrapper)\n place(wrapper, props.clientRect?.())\n },\n onUpdate: (props) => {\n renderer?.updateProps(props)\n if (wrapper) place(wrapper, props.clientRect?.())\n },\n onKeyDown: (props) => {\n if (props.event.key === 'Escape') {\n wrapper?.remove()\n wrapper = null\n return true\n }\n return renderer?.ref?.onKeyDown(props) ?? false\n },\n onExit: () => {\n wrapper?.remove()\n wrapper = null\n renderer?.destroy()\n renderer = null\n },\n }\n },\n }\n}\n\n/** Build the slash-command extension bound to the editor's shared media pickers. */\nexport function createSlashCommand(options: SlashCommandOptions) {\n return Extension.create<SlashCommandOptions>({\n name: 'slashCommand',\n addOptions: () => options,\n addProseMirrorPlugins() {\n return [Suggestion({ editor: this.editor, ...slashSuggestion(this.options) })]\n },\n })\n}\n"]}
1
+ {"version":3,"file":"slash-command.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/slash-command.tsx"],"names":[],"mappings":"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","sourcesContent":["import { forwardRef, useImperativeHandle, useState } from 'react'\nimport type { ComponentType } from 'react'\nimport { Extension, ReactRenderer } from '@tiptap/react'\nimport type { Editor, Range } from '@tiptap/react'\nimport Suggestion from '@tiptap/suggestion'\nimport type { SuggestionKeyDownProps, SuggestionOptions, SuggestionProps } from '@tiptap/suggestion'\nimport { Blocks, Heading1, Heading2, Heading3, ImagePlus, Pilcrow } from 'lucide-react'\nimport type { LucideIcon } from 'lucide-react'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { componentDisplayName } from '@elytracms/studio/lib/persona/labels'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { BLOCK_COMMANDS } from './block-commands'\n\n/**\n * Notion-style slash menu (EC-314): typing `/` at the caret opens a filterable\n * command palette that inserts block-level content — headings, lists, quote, code,\n * divider — and, when available, an image or a registry component.\n *\n * 0.0.41: the embed path shows the permitted components **directly** as their own\n * items under a \"Components\" group (allow-gated exactly like the picker), rather than\n * a single \"Embed component\" item that opened a separate picker dialog — the\n * product-owner feedback that the extra step was unintuitive. Selecting a component\n * inserts it with manifest defaults and selects it, so the sidebar inspector opens.\n * Built on TipTap's `Suggestion` utility; the menu is a React overlay at the caret.\n */\n\n/** How the slash menu inserts a chosen component (owned by the editor). */\nexport interface SlashEmbedOptions {\n /** The permitted component manifests (already `allow`-gated by the field). */\n components: ComponentManifest[]\n /** Insert `manifest` at `range` (deletes the `/query`, inserts + selects). */\n insert: (editor: Editor, range: Range, manifest: ComponentManifest) => void\n}\n\nexport interface SlashCommandOptions {\n /** Open the asset picker (omitted ⇒ no `/image` item). */\n onImage?: () => void\n /** The embeddable components + inserter (omitted/empty ⇒ no component items). */\n embed?: SlashEmbedOptions\n}\n\nexport interface SlashItem {\n key: string\n title: string\n keywords: string\n icon: LucideIcon\n /** Optional group label — consecutive same-group items share a header. */\n group?: string\n run: (editor: Editor, range: Range) => void\n}\n\n/** The command list, gated by which media actions + components the editor provided. */\nexport function buildSlashItems(options: SlashCommandOptions): SlashItem[] {\n const block = (run: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>) =>\n (editor: Editor, range: Range) => run(editor.chain().focus().deleteRange(range)).run()\n\n const items: SlashItem[] = [\n { key: 'text', title: translate('richText.slash.text'), keywords: 'paragraph text plain', icon: Pilcrow, run: block((c) => c.setParagraph()) },\n { key: 'h1', title: translate('richText.heading', { level: 1 }), keywords: 'heading title h1', icon: Heading1, run: block((c) => c.toggleHeading({ level: 1 })) },\n { key: 'h2', title: translate('richText.heading', { level: 2 }), keywords: 'heading subtitle h2', icon: Heading2, run: block((c) => c.toggleHeading({ level: 2 })) },\n { key: 'h3', title: translate('richText.heading', { level: 3 }), keywords: 'heading h3', icon: Heading3, run: block((c) => c.toggleHeading({ level: 3 })) },\n // The shared table (EC-328) — bulletList/orderedList/blockquote/codeBlock/divider,\n // the SAME descriptors `RichTextToolbar` derives its buttons from.\n ...BLOCK_COMMANDS.map((cmd) => ({\n key: cmd.key,\n title: translate(cmd.labelKey),\n keywords: cmd.keywords,\n icon: cmd.icon,\n run: block(cmd.chain),\n })),\n ]\n if (options.onImage) {\n items.push({\n key: 'image',\n title: translate('richText.image.add'),\n keywords: 'image picture photo media',\n icon: ImagePlus,\n run: (editor, range) => {\n editor.chain().focus().deleteRange(range).run()\n options.onImage?.()\n },\n })\n }\n const embed = options.embed\n if (embed) {\n const group = translate('richText.slash.componentsGroup')\n for (const manifest of embed.components) {\n const title = componentDisplayName(manifest.id, manifest)\n items.push({\n key: `embed:${manifest.id}`,\n title,\n keywords: `${title} ${manifest.id} component embed block`.toLowerCase(),\n icon: Blocks,\n group,\n run: (editor, range) => embed.insert(editor, range, manifest),\n })\n }\n }\n return items\n}\n\n/** Filter items by a typed query (title or keyword substring) — empty query passes all. */\nexport function filterSlashItems(items: SlashItem[], query: string): SlashItem[] {\n const q = query.trim().toLowerCase()\n if (!q) return items\n return items.filter((item) => item.title.toLowerCase().includes(q) || item.keywords.includes(q))\n}\n\n/** Imperative handle the suggestion render loop calls for keyboard navigation. */\ninterface SlashMenuHandle {\n onKeyDown: (props: SuggestionKeyDownProps) => boolean\n}\n\nconst SlashMenu = forwardRef<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>>((props, ref) => {\n const [selected, setSelected] = useState(0)\n // Reset the highlight DURING RENDER when `items` changes, not in a post-render\n // Effect (EC-328 fix): `@tiptap/suggestion`'s `createSuggestionView.update()`\n // dispatches a NEW `items` array on nearly every keystroke, so an\n // effect-based reset landed one render late — a frame where `selected` was\n // briefly stale/out-of-range against the freshly filtered list. Comparing\n // against the previous reference here (React's documented \"adjust state\n // during render\" idiom) resets it in the SAME render that receives the new\n // `items`, before anything paints.\n const [prevItems, setPrevItems] = useState(props.items)\n if (props.items !== prevItems) {\n setPrevItems(props.items)\n setSelected(0)\n }\n\n useImperativeHandle(ref, () => ({\n onKeyDown: ({ event }) => {\n const count = props.items.length\n if (count === 0) return false\n if (event.key === 'ArrowUp') {\n setSelected((s) => (s + count - 1) % count)\n return true\n }\n if (event.key === 'ArrowDown') {\n setSelected((s) => (s + 1) % count)\n return true\n }\n if (event.key === 'Enter') {\n const item = props.items[selected]\n if (item) props.command(item)\n return true\n }\n return false\n },\n }))\n\n if (props.items.length === 0) {\n return (\n <div className=\"ec-rte-slash\">\n <span className=\"px-2 py-1.5 text-xs text-muted-foreground\">\n {translate('richText.slash.empty')}\n </span>\n </div>\n )\n }\n\n return (\n <div className=\"ec-rte-slash\" role=\"menu\">\n {props.items.map((item, i) => {\n const Icon = item.icon\n // A group header precedes the first item whose group differs from its\n // predecessor (the block-formatting items are ungrouped; components share one).\n const showHeader = item.group != null && item.group !== props.items[i - 1]?.group\n return (\n <div key={item.key}>\n {showHeader ? (\n <span className=\"block px-2 pt-2 pb-1 text-[0.6875rem] font-medium uppercase tracking-wide text-muted-foreground\">\n {item.group}\n </span>\n ) : null}\n <button\n type=\"button\"\n role=\"menuitem\"\n onMouseEnter={() => setSelected(i)}\n onMouseDown={(e) => {\n e.preventDefault()\n props.command(item)\n }}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-1.5 text-left text-sm outline-none',\n i === selected ? 'bg-muted' : 'hover:bg-muted/60',\n )}\n >\n <Icon className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"truncate\">{item.title}</span>\n </button>\n </div>\n )\n })}\n </div>\n )\n})\nSlashMenu.displayName = 'SlashMenu'\n\n/** Position a floating element under the caret, flipping above near the viewport edge. */\nfunction place(el: HTMLElement, rect: DOMRect | null | undefined): void {\n if (!rect) return\n const margin = 8\n const height = el.offsetHeight || 280\n const below = rect.bottom + 4\n const flip = below + height > window.innerHeight - margin && rect.top - height > margin\n el.style.position = 'fixed'\n el.style.left = `${Math.min(rect.left, window.innerWidth - 260)}px`\n el.style.top = flip ? `${rect.top - height - 4}px` : `${below}px`\n el.style.zIndex = '60'\n}\n\n/** The suggestion config (everything but the `editor`, which the extension injects). */\nfunction slashSuggestion(options: SlashCommandOptions): Omit<SuggestionOptions<SlashItem, SlashItem>, 'editor'> {\n return {\n char: '/',\n startOfLine: false,\n items: ({ query }) => filterSlashItems(buildSlashItems(options), query),\n command: ({ editor, range, props }) => props.run(editor, range),\n render: () => {\n let renderer: ReactRenderer<SlashMenuHandle, SuggestionProps<SlashItem, SlashItem>> | null = null\n let wrapper: HTMLDivElement | null = null\n\n return {\n onStart: (props) => {\n renderer = new ReactRenderer(SlashMenu as ComponentType<SuggestionProps<SlashItem, SlashItem>>, {\n props,\n editor: props.editor,\n })\n wrapper = document.createElement('div')\n wrapper.appendChild(renderer.element)\n document.body.appendChild(wrapper)\n place(wrapper, props.clientRect?.())\n },\n onUpdate: (props) => {\n renderer?.updateProps(props)\n if (wrapper) place(wrapper, props.clientRect?.())\n },\n onKeyDown: (props) => {\n if (props.event.key === 'Escape') {\n wrapper?.remove()\n wrapper = null\n return true\n }\n return renderer?.ref?.onKeyDown(props) ?? false\n },\n onExit: () => {\n wrapper?.remove()\n wrapper = null\n renderer?.destroy()\n renderer = null\n },\n }\n },\n }\n}\n\n/** Build the slash-command extension bound to the editor's shared media pickers. */\nexport function createSlashCommand(options: SlashCommandOptions) {\n return Extension.create<SlashCommandOptions>({\n name: 'slashCommand',\n addOptions: () => options,\n addProseMirrorPlugins() {\n return [Suggestion({ editor: this.editor, ...slashSuggestion(this.options) })]\n },\n })\n}\n"]}
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useEditorState } from '@tiptap/react';
3
- import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, List, ListOrdered, Minus, Pilcrow, Quote, Redo2, SquareCode, Strikethrough, Underline, Undo2, } from 'lucide-react';
3
+ import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, Pilcrow, Redo2, Strikethrough, Underline, Undo2, } from 'lucide-react';
4
4
  import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Toolbar, ToolbarGroup, ToolbarSeparator, } from '@elytracms/studio/ui';
5
5
  import { translate } from '@elytracms/studio/lib/i18n';
6
6
  import { ToolbarButton } from './toolbar-button';
7
7
  import { LinkControl } from './link-control';
8
8
  import { toolbarStateFromEditor } from './toolbar-state';
9
+ import { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands';
9
10
  /**
10
11
  * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that
11
12
  * reads its active/enabled state reactively from the editor (`useEditorState`) so a
@@ -25,6 +26,6 @@ export function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed }) {
25
26
  const styleLabel = state.headingLevel != null
26
27
  ? translate('richText.heading', { level: state.headingLevel })
27
28
  : translate('richText.paragraph');
28
- return (_jsxs(Toolbar, { className: "h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5", children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 gap-1 px-2 font-medium", onMouseDown: (e) => e.preventDefault(), children: [styleLabel, _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "min-w-40", children: [_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().setParagraph().run(), children: [_jsx(Pilcrow, { className: "size-3.5" }), " ", translate('richText.paragraph')] }), [1, 2, 3].map((level) => (_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().toggleHeading({ level }).run(), children: [_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", level] }), ' ', translate('richText.heading', { level })] }, level)))] })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bulletList, label: translate('richText.bulletList'), onClick: () => editor.chain().focus().toggleBulletList().run(), children: _jsx(List, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.orderedList, label: translate('richText.orderedList'), onClick: () => editor.chain().focus().toggleOrderedList().run(), children: _jsx(ListOrdered, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.blockquote, label: translate('richText.blockquote'), onClick: () => editor.chain().focus().toggleBlockquote().run(), children: _jsx(Quote, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.codeBlock, label: translate('richText.codeBlock'), onClick: () => editor.chain().focus().toggleCodeBlock().run(), children: _jsx(SquareCode, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, label: translate('richText.horizontalRule'), onClick: () => editor.chain().focus().setHorizontalRule().run(), children: _jsx(Minus, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] })] }));
29
+ return (_jsxs(Toolbar, { className: "h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5", children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 gap-1 px-2 font-medium", onMouseDown: (e) => e.preventDefault(), children: [styleLabel, _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "min-w-40", children: [_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().setParagraph().run(), children: [_jsx(Pilcrow, { className: "size-3.5" }), " ", translate('richText.paragraph')] }), [1, 2, 3].map((level) => (_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().toggleHeading({ level }).run(), children: [_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", level] }), ' ', translate('richText.heading', { level })] }, level)))] })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: BLOCK_COMMANDS.map((cmd) => (_jsx(ToolbarButton, { active: isBlockCommandActive(cmd, state), label: translate(cmd.labelKey), onClick: () => cmd.chain(editor.chain().focus()).run(), children: _jsx(cmd.icon, { className: "size-3.5" }) }, cmd.key))) }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] })] }));
29
30
  }
30
31
  //# sourceMappingURL=toolbar.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,eAAe,CAAA;AAE9C,OAAO,EACL,MAAM,EACN,IAAI,EACJ,WAAW,EACX,IAAI,EACJ,SAAS,EACT,MAAM,EACN,IAAI,EACJ,WAAW,EACX,KAAK,EACL,OAAO,EACP,KAAK,EACL,KAAK,EACL,UAAU,EACV,aAAa,EACb,SAAS,EACT,KAAK,GACN,MAAM,cAAc,CAAA;AACrB,OAAO,EACL,MAAM,EACN,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,EACnB,OAAO,EACP,YAAY,EACZ,gBAAgB,GACjB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAE5C,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAA;AAYxD;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,EAAE,WAAW,EAAwB;IACjG,gFAAgF;IAChF,gFAAgF;IAChF,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,MAAM,KAAK,GAAG,cAAc,CAAC;QAC3B,MAAM;QACN,QAAQ,EAAE,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC,sBAAsB,CAAC,MAAM,CAAC;KACzD,CAAC,CAAA;IAEF,MAAM,UAAU,GACd,KAAK,CAAC,YAAY,IAAI,IAAI;QACxB,CAAC,CAAC,SAAS,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,YAAY,EAAE,CAAC;QAC9D,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC,CAAA;IAErC,OAAO,CACL,MAAC,OAAO,IAAC,SAAS,EAAC,8EAA8E,aAC/F,MAAC,YAAY,eACX,KAAC,mBAAmB,IAAC,OAAO,kBAC1B,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAC,4BAA4B,EACtC,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE,aAErC,UAAU,EACX,KAAC,WAAW,IAAC,SAAS,EAAC,mBAAmB,GAAG,IACtC,GACW,EACtB,MAAC,mBAAmB,IAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,UAAU,aACrD,MAAC,gBAAgB,IAAC,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,aAC3E,KAAC,OAAO,IAAC,SAAS,EAAC,UAAU,GAAG,OAAE,SAAS,CAAC,oBAAoB,CAAC,IAChD,EACjB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAW,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CACnC,MAAC,gBAAgB,IAEf,QAAQ,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,GAAG,EAAE,aAErE,gBAAM,SAAS,EAAC,yCAAyC,kBAAG,KAAK,IAAQ,EAAC,GAAG,EAC5E,SAAS,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,CAAC,KAJpC,KAAK,CAKO,CACpB,CAAC,IACkB,IACT,EAEf,KAAC,gBAAgB,KAAG,EAEpB,MAAC,YAAY,eACX,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,IAAI,EAClB,KAAK,EAAE,SAAS,CAAC,eAAe,CAAC,EACjC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,YAExD,KAAC,IAAI,IAAC,SAAS,EAAC,UAAU,GAAG,GACf,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,MAAM,EACpB,KAAK,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACnC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,YAE1D,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACjB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,SAAS,EACvB,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,YAE7D,KAAC,SAAS,IAAC,SAAS,EAAC,UAAU,GAAG,GACpB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,MAAM,EACpB,KAAK,EAAE,SAAS,CAAC,iBAAiB,CAAC,EACnC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,YAAY,EAAE,CAAC,GAAG,EAAE,YAE1D,KAAC,aAAa,IAAC,SAAS,EAAC,UAAU,GAAG,GACxB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,IAAI,EAClB,KAAK,EAAE,SAAS,CAAC,eAAe,CAAC,EACjC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,UAAU,EAAE,CAAC,GAAG,EAAE,YAExD,KAAC,IAAI,IAAC,SAAS,EAAC,UAAU,GAAG,GACf,IACH,EAEf,KAAC,gBAAgB,KAAG,EAEpB,MAAC,YAAY,eACX,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,UAAU,EACxB,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC,EACvC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,gBAAgB,EAAE,CAAC,GAAG,EAAE,YAE9D,KAAC,IAAI,IAAC,SAAS,EAAC,UAAU,GAAG,GACf,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,WAAW,EACzB,KAAK,EAAE,SAAS,CAAC,sBAAsB,CAAC,EACxC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,iBAAiB,EAAE,CAAC,GAAG,EAAE,YAE/D,KAAC,WAAW,IAAC,SAAS,EAAC,UAAU,GAAG,GACtB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,UAAU,EACxB,KAAK,EAAE,SAAS,CAAC,qBAAqB,CAAC,EACvC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,gBAAgB,EAAE,CAAC,GAAG,EAAE,YAE9D,KAAC,KAAK,IAAC,SAAS,EAAC,UAAU,GAAG,GAChB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,CAAC,SAAS,EACvB,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,eAAe,EAAE,CAAC,GAAG,EAAE,YAE7D,KAAC,UAAU,IAAC,SAAS,EAAC,UAAU,GAAG,GACrB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,EACb,KAAK,EAAE,SAAS,CAAC,yBAAyB,CAAC,EAC3C,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,iBAAiB,EAAE,CAAC,GAAG,EAAE,YAE/D,KAAC,KAAK,IAAC,SAAS,EAAC,UAAU,GAAG,GAChB,IACH,EAEf,KAAC,gBAAgB,KAAG,EAEpB,MAAC,YAAY,eACX,KAAC,WAAW,IAAC,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAI,EAChD,WAAW,CAAC,CAAC,CAAC,CACb,KAAC,aAAa,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,WAAW,YACzE,KAAC,SAAS,IAAC,SAAS,EAAC,UAAU,GAAG,GACpB,CACjB,CAAC,CAAC,CAAC,IAAI,EACP,WAAW,CAAC,CAAC,CAAC,CACb,KAAC,aAAa,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,WAAW,YACzE,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACjB,CACjB,CAAC,CAAC,CAAC,IAAI,IACK,EAEf,cAAK,SAAS,EAAC,QAAQ,GAAG,EAE1B,MAAC,YAAY,eACX,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,EACb,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,EACxB,KAAK,EAAE,SAAS,CAAC,eAAe,CAAC,EACjC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,YAElD,KAAC,KAAK,IAAC,SAAS,EAAC,UAAU,GAAG,GAChB,EAChB,KAAC,aAAa,IACZ,MAAM,EAAE,KAAK,EACb,QAAQ,EAAE,CAAC,KAAK,CAAC,OAAO,EACxB,KAAK,EAAE,SAAS,CAAC,eAAe,CAAC,EACjC,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,CAAC,GAAG,EAAE,YAElD,KAAC,KAAK,IAAC,SAAS,EAAC,UAAU,GAAG,GAChB,IACH,IACP,CACX,CAAA;AACH,CAAC","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n List,\n ListOrdered,\n Minus,\n Pilcrow,\n Quote,\n Redo2,\n SquareCode,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed }: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n const styleLabel =\n state.headingLevel != null\n ? translate('richText.heading', { level: state.headingLevel })\n : translate('richText.paragraph')\n\n return (\n <Toolbar className=\"h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 gap-1 px-2 font-medium\"\n onMouseDown={(e) => e.preventDefault()}\n >\n {styleLabel}\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n <DropdownMenuItem onSelect={() => editor.chain().focus().setParagraph().run()}>\n <Pilcrow className=\"size-3.5\" /> {translate('richText.paragraph')}\n </DropdownMenuItem>\n {([1, 2, 3] as const).map((level) => (\n <DropdownMenuItem\n key={level}\n onSelect={() => editor.chain().focus().toggleHeading({ level }).run()}\n >\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{level}</span>{' '}\n {translate('richText.heading', { level })}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bulletList}\n label={translate('richText.bulletList')}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.orderedList}\n label={translate('richText.orderedList')}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.blockquote}\n label={translate('richText.blockquote')}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.codeBlock}\n label={translate('richText.codeBlock')}\n onClick={() => editor.chain().focus().toggleCodeBlock().run()}\n >\n <SquareCode className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n label={translate('richText.horizontalRule')}\n onClick={() => editor.chain().focus().setHorizontalRule().run()}\n >\n <Minus className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n </Toolbar>\n )\n}\n"]}
1
+ {"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n Pilcrow,\n Redo2,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\nimport { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed }: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n const styleLabel =\n state.headingLevel != null\n ? translate('richText.heading', { level: state.headingLevel })\n : translate('richText.paragraph')\n\n return (\n <Toolbar className=\"h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5\">\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 gap-1 px-2 font-medium\"\n onMouseDown={(e) => e.preventDefault()}\n >\n {styleLabel}\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n <DropdownMenuItem onSelect={() => editor.chain().focus().setParagraph().run()}>\n <Pilcrow className=\"size-3.5\" /> {translate('richText.paragraph')}\n </DropdownMenuItem>\n {([1, 2, 3] as const).map((level) => (\n <DropdownMenuItem\n key={level}\n onSelect={() => editor.chain().focus().toggleHeading({ level }).run()}\n >\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{level}</span>{' '}\n {translate('richText.heading', { level })}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n {/* The shared table (EC-328) — the SAME descriptors `buildSlashItems`\n derives its bulletList/orderedList/blockquote/codeBlock/divider items from. */}\n {BLOCK_COMMANDS.map((cmd) => (\n <ToolbarButton\n key={cmd.key}\n active={isBlockCommandActive(cmd, state)}\n label={translate(cmd.labelKey)}\n onClick={() => cmd.chain(editor.chain().focus()).run()}\n >\n <cmd.icon className=\"size-3.5\" />\n </ToolbarButton>\n ))}\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n </Toolbar>\n )\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { Dispatch, SetStateAction } from 'react';
2
+ import type { CompareState } from './document-editor-context';
3
+ export interface CompareSelection {
4
+ /** The open version comparison, or null when none (the sticky Compare tab). */
5
+ compare: CompareState | null;
6
+ /** Open (or refocus) the Compare tab with `base` vs the current draft. */
7
+ startCompare: (base: number) => void;
8
+ /** Change which recorded version is the (older) base side. */
9
+ setCompareBase: (base: number) => void;
10
+ /** Change what the base is compared against (a version, or the live draft). */
11
+ setCompareTarget: (target: number | 'draft') => void;
12
+ /** Leave compare mode, back to the first content tab. */
13
+ exitCompare: () => void;
14
+ }
15
+ /**
16
+ * EC-298 — the version-compare selection. The Compare tab is STICKY: opening it
17
+ * sets the selection, focuses the tab and the Versions pane; tabbing to the real
18
+ * fields keeps the comparison open (the selection persists) so an editor can flip
19
+ * between diff and fields; only its ✕ closes it. Owns the selection; the tab focus
20
+ * and the inspector pane it drives are threaded in from the editor shell.
21
+ */
22
+ export declare function useCompareSelection(input: {
23
+ tabs: string[];
24
+ compareTab: string;
25
+ setActiveTab: Dispatch<SetStateAction<string>>;
26
+ setInspectorPaneId: (id: string | undefined) => void;
27
+ }): CompareSelection;
@@ -0,0 +1,29 @@
1
+ import { useCallback, useState } from 'react';
2
+ /**
3
+ * EC-298 — the version-compare selection. The Compare tab is STICKY: opening it
4
+ * sets the selection, focuses the tab and the Versions pane; tabbing to the real
5
+ * fields keeps the comparison open (the selection persists) so an editor can flip
6
+ * between diff and fields; only its ✕ closes it. Owns the selection; the tab focus
7
+ * and the inspector pane it drives are threaded in from the editor shell.
8
+ */
9
+ export function useCompareSelection(input) {
10
+ const { tabs, compareTab, setActiveTab, setInspectorPaneId } = input;
11
+ const [compare, setCompare] = useState(null);
12
+ const startCompare = useCallback((base) => {
13
+ setCompare({ base, target: 'draft' });
14
+ setActiveTab(compareTab);
15
+ setInspectorPaneId('versions');
16
+ }, [compareTab, setActiveTab, setInspectorPaneId]);
17
+ const setCompareBase = useCallback((base) => {
18
+ setCompare((c) => (c ? { ...c, base } : { base, target: 'draft' }));
19
+ }, []);
20
+ const setCompareTarget = useCallback((target) => {
21
+ setCompare((c) => (c ? { ...c, target } : c));
22
+ }, []);
23
+ const exitCompare = useCallback(() => {
24
+ setCompare(null);
25
+ setActiveTab((prev) => (prev === compareTab ? tabs[0] : prev));
26
+ }, [tabs, compareTab, setActiveTab]);
27
+ return { compare, startCompare, setCompareBase, setCompareTarget, exitCompare };
28
+ }
29
+ //# sourceMappingURL=use-compare-selection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-compare-selection.js","sourceRoot":"","sources":["../../../src/components/cms/use-compare-selection.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAiB7C;;;;;;GAMG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAKnC;IACC,MAAM,EAAE,IAAI,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IACpE,MAAM,CAAC,OAAO,EAAE,UAAU,CAAC,GAAG,QAAQ,CAAsB,IAAI,CAAC,CAAA;IACjE,MAAM,YAAY,GAAG,WAAW,CAC9B,CAAC,IAAY,EAAE,EAAE;QACf,UAAU,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAA;QACrC,YAAY,CAAC,UAAU,CAAC,CAAA;QACxB,kBAAkB,CAAC,UAAU,CAAC,CAAA;IAChC,CAAC,EACD,CAAC,UAAU,EAAE,YAAY,EAAE,kBAAkB,CAAC,CAC/C,CAAA;IACD,MAAM,cAAc,GAAG,WAAW,CAAC,CAAC,IAAY,EAAE,EAAE;QAClD,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,CAAC,CAAA;IACrE,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,gBAAgB,GAAG,WAAW,CAAC,CAAC,MAAwB,EAAE,EAAE;QAChE,UAAU,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAA;IAC/C,CAAC,EAAE,EAAE,CAAC,CAAA;IACN,MAAM,WAAW,GAAG,WAAW,CAAC,GAAG,EAAE;QACnC,UAAU,CAAC,IAAI,CAAC,CAAA;QAChB,YAAY,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAA;IACjE,CAAC,EAAE,CAAC,IAAI,EAAE,UAAU,EAAE,YAAY,CAAC,CAAC,CAAA;IAEpC,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,gBAAgB,EAAE,WAAW,EAAE,CAAA;AACjF,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { Dispatch, SetStateAction } from 'react'\nimport type { CompareState } from './document-editor-context'\n\nexport interface CompareSelection {\n /** The open version comparison, or null when none (the sticky Compare tab). */\n compare: CompareState | null\n /** Open (or refocus) the Compare tab with `base` vs the current draft. */\n startCompare: (base: number) => void\n /** Change which recorded version is the (older) base side. */\n setCompareBase: (base: number) => void\n /** Change what the base is compared against (a version, or the live draft). */\n setCompareTarget: (target: number | 'draft') => void\n /** Leave compare mode, back to the first content tab. */\n exitCompare: () => void\n}\n\n/**\n * EC-298 — the version-compare selection. The Compare tab is STICKY: opening it\n * sets the selection, focuses the tab and the Versions pane; tabbing to the real\n * fields keeps the comparison open (the selection persists) so an editor can flip\n * between diff and fields; only its ✕ closes it. Owns the selection; the tab focus\n * and the inspector pane it drives are threaded in from the editor shell.\n */\nexport function useCompareSelection(input: {\n tabs: string[]\n compareTab: string\n setActiveTab: Dispatch<SetStateAction<string>>\n setInspectorPaneId: (id: string | undefined) => void\n}): CompareSelection {\n const { tabs, compareTab, setActiveTab, setInspectorPaneId } = input\n const [compare, setCompare] = useState<CompareState | null>(null)\n const startCompare = useCallback(\n (base: number) => {\n setCompare({ base, target: 'draft' })\n setActiveTab(compareTab)\n setInspectorPaneId('versions')\n },\n [compareTab, setActiveTab, setInspectorPaneId],\n )\n const setCompareBase = useCallback((base: number) => {\n setCompare((c) => (c ? { ...c, base } : { base, target: 'draft' }))\n }, [])\n const setCompareTarget = useCallback((target: number | 'draft') => {\n setCompare((c) => (c ? { ...c, target } : c))\n }, [])\n const exitCompare = useCallback(() => {\n setCompare(null)\n setActiveTab((prev) => (prev === compareTab ? tabs[0]! : prev))\n }, [tabs, compareTab, setActiveTab])\n\n return { compare, startCompare, setCompareBase, setCompareTarget, exitCompare }\n}\n"]}
@@ -0,0 +1,44 @@
1
+ import type { QueryKey } from '@tanstack/react-query';
2
+ import type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core';
3
+ import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
+ import type { DiscardDraftStatus } from './discard-draft';
5
+ export interface DiscardController {
6
+ status: DiscardDraftStatus;
7
+ /** The hint shown when the item is disabled (view-read-only / up-to-date / loading). */
8
+ disabledReason: string | undefined;
9
+ /** Is the confirmation dialog open? */
10
+ confirmOpen: boolean;
11
+ setConfirmOpen: (open: boolean) => void;
12
+ /** Is a discard restore round-tripping (gates the dialog buttons)? */
13
+ busy: boolean;
14
+ /** Confirm the discard: flush → restore the pinned published version whole. */
15
+ discard: () => Promise<void>;
16
+ }
17
+ /**
18
+ * EC-316 / EC-325 — the "Discard changes" controller. Reverting the draft to the
19
+ * published content IS a whole-document restore of the pinned published version,
20
+ * so it routes through the SAME shared restore sequence
21
+ * ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use
22
+ * — never a parallel write path. That sequence flushes a dirty in-memory buffer
23
+ * FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing
24
+ * the undo-guarantee gap the header's discard used to have.
25
+ *
26
+ * The enablement is the pure `discardDraftStatus` decision (tested in
27
+ * `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the
28
+ * Versions pane's publish gate uses, so the two never contradict (EC-298).
29
+ */
30
+ export declare function useDiscardController(input: {
31
+ canDiscard: boolean;
32
+ published: boolean;
33
+ viewReadOnly: boolean;
34
+ publishedSnapshotDoc: CmsDocument | undefined;
35
+ publishedVersionNumber: number | undefined;
36
+ versionKey: QueryKey;
37
+ collection: CollectionDef;
38
+ draft: CmsDocument;
39
+ source: CollectionViewSource;
40
+ docRef: DocumentRef;
41
+ replaceDraft: (doc: CmsDocument) => void;
42
+ dirty: boolean;
43
+ save: () => void;
44
+ }): DiscardController;
@@ -0,0 +1,51 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { translate } from '@elytracms/studio/lib/i18n';
3
+ import { discardDraftStatus } from './discard-draft';
4
+ import { useRestoreVersion } from './use-restore-version';
5
+ /**
6
+ * EC-316 / EC-325 — the "Discard changes" controller. Reverting the draft to the
7
+ * published content IS a whole-document restore of the pinned published version,
8
+ * so it routes through the SAME shared restore sequence
9
+ * ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use
10
+ * — never a parallel write path. That sequence flushes a dirty in-memory buffer
11
+ * FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing
12
+ * the undo-guarantee gap the header's discard used to have.
13
+ *
14
+ * The enablement is the pure `discardDraftStatus` decision (tested in
15
+ * `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the
16
+ * Versions pane's publish gate uses, so the two never contradict (EC-298).
17
+ */
18
+ export function useDiscardController(input) {
19
+ const { canDiscard, published, viewReadOnly, publishedSnapshotDoc, publishedVersionNumber, versionKey, collection, draft, source, docRef, replaceDraft, dirty, save, } = input;
20
+ const [confirmOpen, setConfirmOpen] = useState(false);
21
+ const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft });
22
+ const status = discardDraftStatus({
23
+ canDiscard,
24
+ published,
25
+ viewReadOnly,
26
+ publishedSnapshot: publishedSnapshotDoc,
27
+ collection,
28
+ draft,
29
+ });
30
+ const disabledReason = status === 'view-read-only'
31
+ ? translate('cms.editor.switchToDraftToDiscard')
32
+ : status === 'up-to-date'
33
+ ? translate('cms.editor.discardDraftUpToDateHint')
34
+ : status === 'loading'
35
+ ? translate('common.loading')
36
+ : undefined;
37
+ const discard = useCallback(async () => {
38
+ if (publishedVersionNumber === undefined)
39
+ return;
40
+ try {
41
+ // Whole-document restore of the pinned published version (the shared helper
42
+ // flushes any dirty buffer first, mirroring the Versions pane / Compare view).
43
+ await restore(publishedVersionNumber, { kind: 'all' });
44
+ }
45
+ finally {
46
+ setConfirmOpen(false);
47
+ }
48
+ }, [publishedVersionNumber, restore]);
49
+ return { status, disabledReason, confirmOpen, setConfirmOpen, busy, discard };
50
+ }
51
+ //# sourceMappingURL=use-discard-controller.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-discard-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-discard-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAI7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAEpD,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAA;AAezD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAcpC;IACC,MAAM,EACJ,UAAU,EACV,SAAS,EACT,YAAY,EACZ,oBAAoB,EACpB,sBAAsB,EACtB,UAAU,EACV,UAAU,EACV,KAAK,EACL,MAAM,EACN,MAAM,EACN,YAAY,EACZ,KAAK,EACL,IAAI,GACL,GAAG,KAAK,CAAA;IAET,MAAM,CAAC,WAAW,EAAE,cAAc,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACrD,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAA;IAEtG,MAAM,MAAM,GAAG,kBAAkB,CAAC;QAChC,UAAU;QACV,SAAS;QACT,YAAY;QACZ,iBAAiB,EAAE,oBAAoB;QACvC,UAAU;QACV,KAAK;KACN,CAAC,CAAA;IACF,MAAM,cAAc,GAClB,MAAM,KAAK,gBAAgB;QACzB,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC;QAChD,CAAC,CAAC,MAAM,KAAK,YAAY;YACvB,CAAC,CAAC,SAAS,CAAC,qCAAqC,CAAC;YAClD,CAAC,CAAC,MAAM,KAAK,SAAS;gBACpB,CAAC,CAAC,SAAS,CAAC,gBAAgB,CAAC;gBAC7B,CAAC,CAAC,SAAS,CAAA;IAEnB,MAAM,OAAO,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACrC,IAAI,sBAAsB,KAAK,SAAS;YAAE,OAAM;QAChD,IAAI,CAAC;YACH,4EAA4E;YAC5E,+EAA+E;YAC/E,MAAM,OAAO,CAAC,sBAAsB,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAA;QACxD,CAAC;gBAAS,CAAC;YACT,cAAc,CAAC,KAAK,CAAC,CAAA;QACvB,CAAC;IACH,CAAC,EAAE,CAAC,sBAAsB,EAAE,OAAO,CAAC,CAAC,CAAA;IAErC,OAAO,EAAE,MAAM,EAAE,cAAc,EAAE,WAAW,EAAE,cAAc,EAAE,IAAI,EAAE,OAAO,EAAE,CAAA;AAC/E,CAAC","sourcesContent":["import { useCallback, useState } from 'react'\nimport type { QueryKey } from '@tanstack/react-query'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { discardDraftStatus } from './discard-draft'\nimport type { DiscardDraftStatus } from './discard-draft'\nimport { useRestoreVersion } from './use-restore-version'\n\nexport interface DiscardController {\n status: DiscardDraftStatus\n /** The hint shown when the item is disabled (view-read-only / up-to-date / loading). */\n disabledReason: string | undefined\n /** Is the confirmation dialog open? */\n confirmOpen: boolean\n setConfirmOpen: (open: boolean) => void\n /** Is a discard restore round-tripping (gates the dialog buttons)? */\n busy: boolean\n /** Confirm the discard: flush → restore the pinned published version whole. */\n discard: () => Promise<void>\n}\n\n/**\n * EC-316 / EC-325 — the \"Discard changes\" controller. Reverting the draft to the\n * published content IS a whole-document restore of the pinned published version,\n * so it routes through the SAME shared restore sequence\n * ({@link useRestoreVersion}) the Compare view's and Versions pane's restores use\n * — never a parallel write path. That sequence flushes a dirty in-memory buffer\n * FIRST (`documents.restore` snapshots whatever is currently PERSISTED), closing\n * the undo-guarantee gap the header's discard used to have.\n *\n * The enablement is the pure `discardDraftStatus` decision (tested in\n * `discard-draft.test.ts`); its semantic-equality edge is the SAME comparator the\n * Versions pane's publish gate uses, so the two never contradict (EC-298).\n */\nexport function useDiscardController(input: {\n canDiscard: boolean\n published: boolean\n viewReadOnly: boolean\n publishedSnapshotDoc: CmsDocument | undefined\n publishedVersionNumber: number | undefined\n versionKey: QueryKey\n collection: CollectionDef\n draft: CmsDocument\n source: CollectionViewSource\n docRef: DocumentRef\n replaceDraft: (doc: CmsDocument) => void\n dirty: boolean\n save: () => void\n}): DiscardController {\n const {\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshotDoc,\n publishedVersionNumber,\n versionKey,\n collection,\n draft,\n source,\n docRef,\n replaceDraft,\n dirty,\n save,\n } = input\n\n const [confirmOpen, setConfirmOpen] = useState(false)\n const { restore, busy } = useRestoreVersion({ source, docRef, versionKey, dirty, save, replaceDraft })\n\n const status = discardDraftStatus({\n canDiscard,\n published,\n viewReadOnly,\n publishedSnapshot: publishedSnapshotDoc,\n collection,\n draft,\n })\n const disabledReason =\n status === 'view-read-only'\n ? translate('cms.editor.switchToDraftToDiscard')\n : status === 'up-to-date'\n ? translate('cms.editor.discardDraftUpToDateHint')\n : status === 'loading'\n ? translate('common.loading')\n : undefined\n\n const discard = useCallback(async () => {\n if (publishedVersionNumber === undefined) return\n try {\n // Whole-document restore of the pinned published version (the shared helper\n // flushes any dirty buffer first, mirroring the Versions pane / Compare view).\n await restore(publishedVersionNumber, { kind: 'all' })\n } finally {\n setConfirmOpen(false)\n }\n }, [publishedVersionNumber, restore])\n\n return { status, disabledReason, confirmOpen, setConfirmOpen, busy, discard }\n}\n"]}
@@ -0,0 +1,12 @@
1
+ import type { EmbedInspectorApi, EmbedInspectorPayload } from './rich-text/embed-inspector-context';
2
+ /**
3
+ * 0.0.41 — the selection-driven embed-inspector host. A `componentEmbed` node
4
+ * selection in a rich-text field swaps the inspector sidebar for the embed's prop
5
+ * form; deselect returns the normal inspector. `clearActiveEmbed` keys on the node
6
+ * id so a fast A→B reselect can't let A's deselect wipe B (the selection race). Fed
7
+ * by the rich-text field through the EmbedInspector context (the returned api).
8
+ */
9
+ export declare function useEmbedInspectorHost(): {
10
+ activeEmbed: EmbedInspectorPayload | null;
11
+ embedInspectorApi: EmbedInspectorApi;
12
+ };