@elytracms/studio 0.0.63 → 0.0.65
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.
- package/convex/assets.ts +11 -0
- package/convex/delivery.ts +5 -0
- package/convex/schema.ts +23 -0
- package/dist/components/assets/asset-thumb.d.ts +4 -2
- package/dist/components/assets/asset-thumb.js +26 -4
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/assets/assets-module.js +48 -2
- package/dist/components/assets/assets-module.js.map +1 -1
- package/dist/components/assets/assets-store.d.ts +11 -1
- package/dist/components/assets/assets-store.js +18 -1
- package/dist/components/assets/assets-store.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +26 -8
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.d.ts +54 -0
- package/dist/components/builder/canvas-chrome-css.js +246 -0
- package/dist/components/builder/canvas-chrome-css.js.map +1 -0
- package/dist/components/builder/canvas-chrome.d.ts +122 -18
- package/dist/components/builder/canvas-chrome.js +148 -82
- package/dist/components/builder/canvas-chrome.js.map +1 -1
- package/dist/components/builder/canvas-inline-editing.d.ts +7 -0
- package/dist/components/builder/canvas-inline-editing.js +3 -1
- package/dist/components/builder/canvas-inline-editing.js.map +1 -1
- package/dist/components/builder/canvas-overlay-geometry.d.ts +72 -0
- package/dist/components/builder/canvas-overlay-geometry.js +80 -0
- package/dist/components/builder/canvas-overlay-geometry.js.map +1 -0
- package/dist/components/builder/canvas-rect-tracking.d.ts +124 -0
- package/dist/components/builder/canvas-rect-tracking.js +198 -0
- package/dist/components/builder/canvas-rect-tracking.js.map +1 -0
- package/dist/components/builder/canvas-render-context.d.ts +13 -0
- package/dist/components/builder/canvas-render-context.js +7 -5
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/canvas-stylesheet.d.ts +1 -1
- package/dist/components/builder/canvas-stylesheet.js +16 -9
- package/dist/components/builder/canvas-stylesheet.js.map +1 -1
- package/dist/components/builder/canvas-theme-notice.d.ts +6 -0
- package/dist/components/builder/canvas-theme-notice.js +46 -0
- package/dist/components/builder/canvas-theme-notice.js.map +1 -0
- package/dist/components/builder/canvas.js +78 -22
- package/dist/components/builder/canvas.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +22 -3
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/delete-key.d.ts +38 -0
- package/dist/components/builder/delete-key.js +46 -0
- package/dist/components/builder/delete-key.js.map +1 -0
- package/dist/components/builder/drag-session.d.ts +8 -0
- package/dist/components/builder/drag-session.js +15 -0
- package/dist/components/builder/drag-session.js.map +1 -1
- package/dist/components/builder/insert-affordance-context.d.ts +19 -0
- package/dist/components/builder/insert-affordance-context.js +7 -0
- package/dist/components/builder/insert-affordance-context.js.map +1 -0
- package/dist/components/builder/insert-context.d.ts +40 -0
- package/dist/components/builder/insert-context.js +86 -3
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/node-title.d.ts +60 -0
- package/dist/components/builder/node-title.js +135 -0
- package/dist/components/builder/node-title.js.map +1 -0
- package/dist/components/builder/page-tree.js +21 -3
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/prop-selection.d.ts +52 -0
- package/dist/components/builder/prop-selection.js +38 -0
- package/dist/components/builder/prop-selection.js.map +1 -0
- package/dist/components/builder/prop-sync-context.d.ts +49 -0
- package/dist/components/builder/prop-sync-context.js +23 -0
- package/dist/components/builder/prop-sync-context.js.map +1 -0
- package/dist/components/builder/property-inspector.js +23 -1
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +1 -1
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/rich-text-toolbar.d.ts +23 -0
- package/dist/components/builder/rich-text-toolbar.js +24 -17
- package/dist/components/builder/rich-text-toolbar.js.map +1 -1
- package/dist/components/builder/selection-overlay.d.ts +79 -0
- package/dist/components/builder/selection-overlay.js +244 -0
- package/dist/components/builder/selection-overlay.js.map +1 -0
- package/dist/components/builder/spacing-handles.d.ts +88 -1
- package/dist/components/builder/spacing-handles.js +191 -63
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-picker.js +11 -5
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/rich-text/block-style.d.ts +48 -0
- package/dist/components/cms/rich-text/block-style.js +30 -0
- package/dist/components/cms/rich-text/block-style.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.js +4 -3
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/extensions.d.ts +2 -0
- package/dist/components/cms/rich-text/extensions.js +4 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -1
- package/dist/components/cms/rich-text/font-size-control.d.ts +31 -0
- package/dist/components/cms/rich-text/font-size-control.js +40 -0
- package/dist/components/cms/rich-text/font-size-control.js.map +1 -0
- package/dist/components/cms/rich-text/font-size.d.ts +16 -0
- package/dist/components/cms/rich-text/font-size.js +40 -0
- package/dist/components/cms/rich-text/font-size.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-state.d.ts +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js +3 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar.d.ts +13 -0
- package/dist/components/cms/rich-text/toolbar.js +17 -5
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/components/component-detail.js +3 -3
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +2 -1
- package/dist/components/components/component-preview.js +25 -26
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/components/components/registry-browser.js +2 -2
- package/dist/components/components/registry-browser.js.map +1 -1
- package/dist/components/components/taxonomy.js +3 -2
- package/dist/components/components/taxonomy.js.map +1 -1
- package/dist/components/components/transfer-dialog.js +1 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/lib/assets/backfill-thumbs.d.ts +67 -0
- package/dist/lib/assets/backfill-thumbs.js +120 -0
- package/dist/lib/assets/backfill-thumbs.js.map +1 -0
- package/dist/lib/assets/delete-asset.d.ts +2 -1
- package/dist/lib/assets/delete-asset.js +12 -0
- package/dist/lib/assets/delete-asset.js.map +1 -1
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/assets/thumbnail.d.ts +145 -0
- package/dist/lib/assets/thumbnail.js +220 -0
- package/dist/lib/assets/thumbnail.js.map +1 -0
- package/dist/lib/assets/upload.d.ts +9 -0
- package/dist/lib/assets/upload.js +31 -0
- package/dist/lib/assets/upload.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +20 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +18 -0
- package/dist/lib/i18n/messages.en.js +21 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/backend-mode.d.ts +7 -0
- package/dist/lib/persistence/backend-mode.js +9 -0
- package/dist/lib/persistence/backend-mode.js.map +1 -1
- package/dist/lib/persistence/convex-adapter.js +4 -0
- package/dist/lib/persistence/convex-adapter.js.map +1 -1
- package/dist/lib/persistence/index.d.ts +1 -1
- package/dist/lib/persistence/index.js +1 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +9 -5
- package/dist/lib/persona/labels.js +13 -8
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/references-store.js +7 -1
- package/dist/lib/references/references-store.js.map +1 -1
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode, isStaticProp } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useBuilder } from './builder-store'\nimport { buildReusableValues, makeEmbedNode, resolveReuseCapability } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = manifests.find((m) => m.id === node.componentId)?.title\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'))\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n if (!node || !collection) {\n setPending(null)\n return\n }\n // Mint the reusable document, then write its title + content (createDocument\n // only seeds defaults). The new id is referenced by the embed below.\n const ref = cms.createDocument(cap.collectionId)\n const values = buildReusableValues(collection, cap, name, node, cms.documentsOf(cap.collectionId))\n cms.saveDocument({ collection: cap.collectionId, id: ref.id, values })\n // Replace the selection in place with a transcluding embed (the action\n // clones to a fresh id; the source `node` stays untouched in the new doc).\n builder.dispatch({ type: 'replace', id: pending.nodeId, nodes: [makeEmbedNode(cap, ref.id)] })\n setPending(null)\n // EC-300: the new reusable is a separate document with its own lifecycle. A\n // draft-only target resolves to nothing in the published perspective (delivery\n // + the published canvas view), so the editor chooses at creation: PUBLISH keeps\n // the section visible everywhere immediately (the continuity-preserving default);\n // SAVE DRAFT keeps it editable, visible in the draft canvas, surfaced as a\n // pending reference until published. No silent auto-publish (AD-16).\n if (publish) {\n void cms.publishDocument(ref).then(\n () => toast.success(translate('builder.reuse.madeReusablePublished', { name })),\n () => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name })),\n )\n } else {\n toast.success(translate('builder.reuse.madeReusableDraft', { name }))\n }\n },\n [capability, pending, builder, cms],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = isStaticProp(prop) ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error(translate('builder.reuse.noTargetToDetach'))\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error(translate('builder.reuse.noContentToDetach'))\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success(translate('builder.reuse.detachedToCopy'))\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('builder.reuse.makeReusable')}</DialogTitle>\n <DialogDescription>\n {translate('builder.reuse.makeReusableDescription')}\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label={translate('builder.reuse.nameLabel')} htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={translate('builder.reuse.namePlaceholder')}\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n {translate('builder.reuse.saveDraft')}\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n {translate('builder.reuse.publish')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reuse-context.js","sourceRoot":"","sources":["../../../src/components/builder/reuse-context.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { FormEvent, ReactNode } from 'react'\nimport { blockRootsOf, getNode, isStaticProp } from '@elytracms/core/project-graph'\nimport { readDocumentRef } from '@elytracms/core/cms-core'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Field,\n Input,\n Stack,\n toast,\n} from '@elytracms/studio/ui'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { useCms } from '../cms/cms-store'\nimport { useBuilder } from './builder-store'\nimport { buildReusableValues, makeEmbedNode, resolveReuseCapability } from './reuse'\nimport type { ReuseCapability } from './reuse'\n\n/**\n * In-canvas reuse gestures (EC-282): \"Make reusable\" promotes a selected block to\n * a new reusable document and replaces it in place with a transcluding embed;\n * \"Detach to local copy\" inlines an embed's referenced blocks so a page can\n * diverge. Both round-trip through the existing persistence + in-Convex reference\n * index (EC-277), so where-used is correct immediately.\n *\n * The provider needs BOTH the builder store (the edited composition) and the CMS\n * store (to create/read the reusable document), so it mounts inside the\n * composition-field editor's `BuilderProvider`, itself inside the CMS document\n * editor. Where no provider is mounted (or the capability is absent), the menu\n * reads `null` and simply omits the gestures.\n */\nexport interface ReuseGestures {\n capability: ReuseCapability | null\n canMakeReusable: (nodeId: string) => boolean\n canDetach: (nodeId: string) => boolean\n /** Open the name dialog for promoting `nodeId`. */\n startMakeReusable: (nodeId: string) => void\n /** Replace a `Reusable` embed with an editable inline copy of its target's blocks. */\n detach: (nodeId: string) => void\n}\n\nconst ReuseContext = createContext<ReuseGestures | null>(null)\n\nexport function useReuse(): ReuseGestures | null {\n return useContext(ReuseContext)\n}\n\nexport function ReuseProvider({ children }: { children: ReactNode }) {\n const builder = useBuilder()\n const cms = useCms()\n const [pending, setPending] = useState<{ nodeId: string; defaultName: string } | null>(null)\n\n const manifests = useMemo(() => getStudioRegistry().registry.list(), [])\n const capability = useMemo(\n () => resolveReuseCapability(manifests, cms.collections),\n [manifests, cms.collections],\n )\n\n const canMakeReusable = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n const node = getNode(builder.present, nodeId)\n // Anything but the synthetic composition root can be promoted; nesting a\n // reusable inside a reusable is allowed (the renderer + bake are cycle-\n // guarded, EC-281), so an embed itself is still promotable.\n return node !== undefined && node.id !== builder.present.id\n },\n [capability, builder],\n )\n\n const canDetach = useCallback(\n (nodeId: string): boolean => {\n if (!capability) return false\n return getNode(builder.present, nodeId)?.componentId === capability.embedComponentId\n },\n [capability, builder],\n )\n\n const startMakeReusable = useCallback(\n (nodeId: string) => {\n if (!canMakeReusable(nodeId)) return\n const node = getNode(builder.present, nodeId)\n if (!node) return\n const manifestTitle = localizedLabel(manifests.find((m) => m.id === node.componentId)?.title) || undefined\n const collection = capability ? cms.getCollection(capability.collectionId) : undefined\n const defaultName =\n manifestTitle ?? (localizedLabel(collection?.form?.labelSingular) || localizedLabel(collection?.form?.label) || translate('builder.reuse.defaultElementName'))\n setPending({ nodeId, defaultName })\n },\n [canMakeReusable, builder, manifests, capability, cms],\n )\n\n const confirmMakeReusable = useCallback(\n (name: string, publish: boolean) => {\n const cap = capability\n if (!cap || !pending) return\n const node = getNode(builder.present, pending.nodeId)\n const collection = cms.getCollection(cap.collectionId)\n if (!node || !collection) {\n setPending(null)\n return\n }\n // Mint the reusable document, then write its title + content (createDocument\n // only seeds defaults). The new id is referenced by the embed below.\n const ref = cms.createDocument(cap.collectionId)\n const values = buildReusableValues(collection, cap, name, node, cms.documentsOf(cap.collectionId))\n cms.saveDocument({ collection: cap.collectionId, id: ref.id, values })\n // Replace the selection in place with a transcluding embed (the action\n // clones to a fresh id; the source `node` stays untouched in the new doc).\n builder.dispatch({ type: 'replace', id: pending.nodeId, nodes: [makeEmbedNode(cap, ref.id)] })\n setPending(null)\n // EC-300: the new reusable is a separate document with its own lifecycle. A\n // draft-only target resolves to nothing in the published perspective (delivery\n // + the published canvas view), so the editor chooses at creation: PUBLISH keeps\n // the section visible everywhere immediately (the continuity-preserving default);\n // SAVE DRAFT keeps it editable, visible in the draft canvas, surfaced as a\n // pending reference until published. No silent auto-publish (AD-16).\n if (publish) {\n void cms.publishDocument(ref).then(\n () => toast.success(translate('builder.reuse.madeReusablePublished', { name })),\n () => toast.error(translate('builder.reuse.madeReusablePublishFailed', { name })),\n )\n } else {\n toast.success(translate('builder.reuse.madeReusableDraft', { name }))\n }\n },\n [capability, pending, builder, cms],\n )\n\n const detach = useCallback(\n (nodeId: string) => {\n const cap = capability\n if (!cap) return\n const node = getNode(builder.present, nodeId)\n if (!node || node.componentId !== cap.embedComponentId) return\n const prop = node.props?.[cap.relationProp]\n const ref = isStaticProp(prop) ? readDocumentRef(prop.value) : undefined\n if (!ref) {\n toast.error(translate('builder.reuse.noTargetToDetach'))\n return\n }\n const target = cms.getDocument(ref)\n const blocks = blockRootsOf(target?.values?.[cap.contentField])\n if (blocks.length === 0) {\n toast.error(translate('builder.reuse.noContentToDetach'))\n return\n }\n builder.dispatch({ type: 'replace', id: nodeId, nodes: blocks })\n toast.success(translate('builder.reuse.detachedToCopy'))\n },\n [capability, builder, cms],\n )\n\n const value = useMemo<ReuseGestures>(\n () => ({ capability, canMakeReusable, canDetach, startMakeReusable, detach }),\n [capability, canMakeReusable, canDetach, startMakeReusable, detach],\n )\n\n return (\n <ReuseContext.Provider value={value}>\n {children}\n <MakeReusableDialog\n open={pending !== null}\n defaultName={pending?.defaultName ?? ''}\n onConfirm={confirmMakeReusable}\n onCancel={() => setPending(null)}\n />\n </ReuseContext.Provider>\n )\n}\n\n/** Name-and-create dialog for the \"Make reusable\" gesture. */\nfunction MakeReusableDialog({\n open,\n defaultName,\n onConfirm,\n onCancel,\n}: {\n open: boolean\n defaultName: string\n /** `publish` chooses the new reusable's initial state (EC-300/AD-16). */\n onConfirm: (name: string, publish: boolean) => void\n onCancel: () => void\n}) {\n const [name, setName] = useState(defaultName)\n // Re-seed the field each time the dialog opens for a new selection.\n const [seededFor, setSeededFor] = useState<string | null>(null)\n if (open && seededFor !== defaultName) {\n setName(defaultName)\n setSeededFor(defaultName)\n }\n\n const empty = name.trim() === ''\n function commit(publish: boolean) {\n if (empty) return\n onConfirm(name.trim(), publish)\n setSeededFor(null)\n }\n\n return (\n <Dialog\n open={open}\n onOpenChange={(next) => {\n if (!next) {\n onCancel()\n setSeededFor(null)\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{translate('builder.reuse.makeReusable')}</DialogTitle>\n <DialogDescription>\n {translate('builder.reuse.makeReusableDescription')}\n </DialogDescription>\n </DialogHeader>\n {/* Enter submits the primary action (Publish) — the continuity-preserving default. */}\n <form\n onSubmit={(event: FormEvent) => {\n event.preventDefault()\n commit(true)\n }}\n >\n <Stack gap=\"3\">\n <Field label={translate('builder.reuse.nameLabel')} htmlFor=\"make-reusable-name\" required>\n <Input\n id=\"make-reusable-name\"\n value={name}\n onChange={(event) => setName(event.target.value)}\n placeholder={translate('builder.reuse.namePlaceholder')}\n autoFocus\n required\n />\n </Field>\n </Stack>\n <DialogFooter className=\"mt-4\">\n {/* Cancel pinned left; the two commit actions group right. */}\n <Button type=\"button\" variant=\"ghost\" size=\"sm\" className=\"mr-auto\" onClick={onCancel}>\n {translate('common.cancel')}\n </Button>\n <Button\n type=\"button\"\n variant=\"secondary\"\n size=\"sm\"\n disabled={empty}\n onClick={() => commit(false)}\n data-testid=\"make-reusable-draft\"\n >\n {translate('builder.reuse.saveDraft')}\n </Button>\n <Button\n type=\"submit\"\n size=\"sm\"\n disabled={empty}\n data-testid=\"make-reusable-publish\"\n >\n {translate('builder.reuse.publish')}\n </Button>\n </DialogFooter>\n </form>\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -1,3 +1,26 @@
|
|
|
1
|
+
/** Everything the portal-free toolbar markup needs (chrome contract surface). */
|
|
2
|
+
export interface RichTextToolbarViewProps {
|
|
3
|
+
left: number;
|
|
4
|
+
top: number;
|
|
5
|
+
bold: boolean;
|
|
6
|
+
italic: boolean;
|
|
7
|
+
hasLink: boolean;
|
|
8
|
+
/** Non-null → the link editor row is open with this draft value. */
|
|
9
|
+
linkDraft: string | null;
|
|
10
|
+
onToolbarMouseDown: (e: React.MouseEvent<HTMLDivElement>) => void;
|
|
11
|
+
onBold: () => void;
|
|
12
|
+
onItalic: () => void;
|
|
13
|
+
onOpenLink: () => void;
|
|
14
|
+
onUnlink: () => void;
|
|
15
|
+
onLinkDraftChange: (value: string) => void;
|
|
16
|
+
onLinkSubmit: () => void;
|
|
17
|
+
onLinkCancel: () => void;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The toolbar's rendered markup, portal-free so the AD-D chrome contract test
|
|
21
|
+
* can walk it with `renderToStaticMarkup`: every class here MUST be `ec-*`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function RichTextToolbarView(props: RichTextToolbarViewProps): import("react").JSX.Element;
|
|
1
24
|
export declare function RichTextToolbar({ doc, host }: {
|
|
2
25
|
doc: Document;
|
|
3
26
|
host: HTMLElement;
|
|
@@ -23,6 +23,28 @@ function selectionState(doc, host) {
|
|
|
23
23
|
linkHref: link?.getAttribute('href') ?? null,
|
|
24
24
|
};
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The toolbar's rendered markup, portal-free so the AD-D chrome contract test
|
|
28
|
+
* can walk it with `renderToStaticMarkup`: every class here MUST be `ec-*`.
|
|
29
|
+
*/
|
|
30
|
+
export function RichTextToolbarView(props) {
|
|
31
|
+
return (_jsx("div", { "data-ec-rt-toolbar": "", role: "toolbar", "aria-label": translate('builder.richText.toolbarLabel'), className: "ec-chrome ec-rt-toolbar", style: {
|
|
32
|
+
position: 'fixed',
|
|
33
|
+
left: Math.max(8, props.left),
|
|
34
|
+
top: Math.max(8, props.top - TOOLBAR_OFFSET_PX),
|
|
35
|
+
transform: 'translate(-50%, -100%)',
|
|
36
|
+
zIndex: 50,
|
|
37
|
+
}, onMouseDown: props.onToolbarMouseDown, children: props.linkDraft !== null ? (_jsxs("form", { className: "ec-rt-toolbar-row", onSubmit: (e) => {
|
|
38
|
+
e.preventDefault();
|
|
39
|
+
props.onLinkSubmit();
|
|
40
|
+
}, children: [_jsx("input", { autoFocus: true, type: "text", value: props.linkDraft, placeholder: translate('builder.richText.linkPlaceholder'), "aria-label": translate('builder.richText.linkUrl'), onChange: (e) => props.onLinkDraftChange(e.target.value), onKeyDown: (e) => {
|
|
41
|
+
if (e.key === 'Escape') {
|
|
42
|
+
e.preventDefault();
|
|
43
|
+
e.stopPropagation();
|
|
44
|
+
props.onLinkCancel();
|
|
45
|
+
}
|
|
46
|
+
}, className: "ec-rt-toolbar-input" }), _jsx("button", { type: "submit", className: "ec-rt-toolbar-btn", "aria-label": translate('builder.richText.applyLink'), children: translate('builder.richText.set') })] })) : (_jsxs("div", { className: "ec-rt-toolbar-row", children: [_jsx(MarkButton, { label: translate('builder.richText.bold'), active: props.bold, onClick: props.onBold, children: "B" }), _jsx(MarkButton, { label: translate('builder.richText.italic'), active: props.italic, onClick: props.onItalic, children: "I" }), _jsx(MarkButton, { label: translate('builder.richText.link'), active: props.hasLink, onClick: props.onOpenLink, children: translate('builder.richText.link') }), props.hasLink ? (_jsx(MarkButton, { label: translate('builder.richText.removeLink'), active: false, onClick: props.onUnlink, children: translate('builder.richText.unlink') })) : null] })) }));
|
|
47
|
+
}
|
|
26
48
|
export function RichTextToolbar({ doc, host }) {
|
|
27
49
|
const [state, setState] = useState(null);
|
|
28
50
|
const [linkOpen, setLinkOpen] = useState(false);
|
|
@@ -78,26 +100,11 @@ export function RichTextToolbar({ doc, host }) {
|
|
|
78
100
|
return null;
|
|
79
101
|
const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0;
|
|
80
102
|
const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0;
|
|
81
|
-
return createPortal(_jsx(
|
|
82
|
-
position: 'fixed',
|
|
83
|
-
left: Math.max(8, left),
|
|
84
|
-
top: Math.max(8, top - TOOLBAR_OFFSET_PX),
|
|
85
|
-
transform: 'translate(-50%, -100%)',
|
|
86
|
-
zIndex: 50,
|
|
87
|
-
}, onMouseDown: (e) => {
|
|
103
|
+
return createPortal(_jsx(RichTextToolbarView, { left: left, top: top, bold: state?.bold ?? false, italic: state?.italic ?? false, hasLink: state !== null && state.linkHref !== null, linkDraft: linkOpen ? linkDraft : null, onToolbarMouseDown: (e) => {
|
|
88
104
|
// Keep the host's selection/focus — except for the link input.
|
|
89
105
|
if (!e.target.closest('input'))
|
|
90
106
|
e.preventDefault();
|
|
91
|
-
},
|
|
92
|
-
e.preventDefault();
|
|
93
|
-
applyLink(linkDraft);
|
|
94
|
-
}, children: [_jsx("input", { autoFocus: true, type: "text", value: linkDraft, placeholder: translate('builder.richText.linkPlaceholder'), "aria-label": translate('builder.richText.linkUrl'), onChange: (e) => setLinkDraft(e.target.value), onKeyDown: (e) => {
|
|
95
|
-
if (e.key === 'Escape') {
|
|
96
|
-
e.preventDefault();
|
|
97
|
-
e.stopPropagation();
|
|
98
|
-
closeLinkEditor();
|
|
99
|
-
}
|
|
100
|
-
}, className: "ec-rt-toolbar-input" }), _jsx("button", { type: "submit", className: "ec-rt-toolbar-btn", "aria-label": translate('builder.richText.applyLink'), children: translate('builder.richText.set') })] })) : (_jsxs("div", { className: "ec-rt-toolbar-row", children: [_jsx(MarkButton, { label: translate('builder.richText.bold'), active: state?.bold ?? false, onClick: () => exec('bold'), children: "B" }), _jsx(MarkButton, { label: translate('builder.richText.italic'), active: state?.italic ?? false, onClick: () => exec('italic'), children: "I" }), _jsx(MarkButton, { label: translate('builder.richText.link'), active: state !== null && state.linkHref !== null, onClick: openLinkEditor, children: translate('builder.richText.link') }), state !== null && state.linkHref !== null ? (_jsx(MarkButton, { label: translate('builder.richText.removeLink'), active: false, onClick: () => exec('unlink'), children: translate('builder.richText.unlink') })) : null] })) }), doc.body);
|
|
107
|
+
}, onBold: () => exec('bold'), onItalic: () => exec('italic'), onOpenLink: openLinkEditor, onUnlink: () => exec('unlink'), onLinkDraftChange: setLinkDraft, onLinkSubmit: () => applyLink(linkDraft), onLinkCancel: closeLinkEditor }), doc.body);
|
|
101
108
|
}
|
|
102
109
|
function MarkButton({ label, active, onClick, children, }) {
|
|
103
110
|
return (_jsx("button", { type: "button", "aria-label": label, "aria-pressed": active, title: label, onClick: onClick, className: "ec-rt-toolbar-btn", "data-active": active || undefined, children: children }));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rich-text-toolbar.js","sourceRoot":"","sources":["../../../src/components/builder/rich-text-toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Floating mark toolbar for inline rich-text editing (EC-159): anchored to the\n * live text selection inside the canvas iframe, offering bold, italic, and\n * link (set + remove). Portaled into the *iframe* document — selection\n * rectangles are iframe-viewport coordinates, so the toolbar positions\n * `fixed` in that viewport and never needs cross-frame math.\n *\n * Mark application uses the document's native editing commands on the live\n * selection (`bold`/`italic`/`createLink`/`unlink`), which produce exactly the\n * tags the `@elytracms/core/rich-text` DOM parser inverts (`b/strong`, `i/em`,\n * `a[href]`). Pointer-down on the toolbar is swallowed so the host keeps its\n * selection; the link input is the one exception (it needs focus), and the\n * canvas blur handler ignores blurs into `[data-ec-rt-toolbar]`.\n */\n\ninterface ToolbarState {\n left: number\n top: number\n bold: boolean\n italic: boolean\n /** The href of the link the selection sits inside, if any. */\n linkHref: string | null\n}\n\nconst TOOLBAR_OFFSET_PX = 8\n\nfunction selectionState(doc: Document, host: HTMLElement): ToolbarState | null {\n const selection = doc.getSelection()\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null\n if (!host.contains(selection.anchorNode) || !host.contains(selection.focusNode)) return null\n const rect = selection.getRangeAt(0).getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n const anchor = selection.anchorNode\n const anchorElement = anchor instanceof Element ? anchor : anchor?.parentElement\n const link = anchorElement?.closest('a') ?? null\n return {\n left: rect.left + rect.width / 2,\n top: rect.top,\n bold: doc.queryCommandState('bold'),\n italic: doc.queryCommandState('italic'),\n linkHref: link?.getAttribute('href') ?? null,\n }\n}\n\nexport function RichTextToolbar({ doc, host }: { doc: Document; host: HTMLElement }) {\n const [state, setState] = useState<ToolbarState | null>(null)\n const [linkOpen, setLinkOpen] = useState(false)\n const [linkDraft, setLinkDraft] = useState('')\n const savedRangeRef = useRef<Range | null>(null)\n\n const refresh = useCallback(() => {\n setState(selectionState(doc, host))\n }, [doc, host])\n\n useEffect(() => {\n refresh()\n doc.addEventListener('selectionchange', refresh)\n return () => doc.removeEventListener('selectionchange', refresh)\n }, [doc, refresh])\n\n // Close a stale link editor when the selection leaves the host entirely.\n useEffect(() => {\n if (!state && !linkOpen) setLinkDraft('')\n }, [state, linkOpen])\n\n const exec = useCallback(\n (command: string, value?: string) => {\n doc.execCommand(command, false, value)\n refresh()\n },\n [doc, refresh],\n )\n\n const openLinkEditor = useCallback(() => {\n const selection = doc.getSelection()\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange()\n }\n setLinkDraft(state?.linkHref ?? '')\n setLinkOpen(true)\n }, [doc, state])\n\n const applyLink = useCallback(\n (href: string) => {\n const selection = doc.getSelection()\n const saved = savedRangeRef.current\n if (saved && selection) {\n selection.removeAllRanges()\n selection.addRange(saved)\n }\n const trimmed = href.trim()\n if (trimmed.length > 0) exec('createLink', trimmed)\n else exec('unlink')\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n },\n [doc, exec, host],\n )\n\n const closeLinkEditor = useCallback(() => {\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n }, [host])\n\n if (!state && !linkOpen) return null\n const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0\n const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0\n\n return createPortal(\n <div\n data-ec-rt-toolbar=\"\"\n role=\"toolbar\"\n aria-label={translate('builder.richText.toolbarLabel')}\n className=\"ec-rt-toolbar\"\n style={{\n position: 'fixed',\n left: Math.max(8, left),\n top: Math.max(8, top - TOOLBAR_OFFSET_PX),\n transform: 'translate(-50%, -100%)',\n zIndex: 50,\n }}\n onMouseDown={(e) => {\n // Keep the host's selection/focus — except for the link input.\n if (!(e.target as HTMLElement).closest('input')) e.preventDefault()\n }}\n >\n {linkOpen ? (\n <form\n className=\"ec-rt-toolbar-row\"\n onSubmit={(e) => {\n e.preventDefault()\n applyLink(linkDraft)\n }}\n >\n <input\n autoFocus\n type=\"text\"\n value={linkDraft}\n placeholder={translate('builder.richText.linkPlaceholder')}\n aria-label={translate('builder.richText.linkUrl')}\n onChange={(e) => setLinkDraft(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n closeLinkEditor()\n }\n }}\n className=\"ec-rt-toolbar-input\"\n />\n <button type=\"submit\" className=\"ec-rt-toolbar-btn\" aria-label={translate('builder.richText.applyLink')}>\n {translate('builder.richText.set')}\n </button>\n </form>\n ) : (\n <div className=\"ec-rt-toolbar-row\">\n <MarkButton label={translate('builder.richText.bold')} active={state?.bold ?? false} onClick={() => exec('bold')}>\n B\n </MarkButton>\n <MarkButton label={translate('builder.richText.italic')} active={state?.italic ?? false} onClick={() => exec('italic')}>\n I\n </MarkButton>\n <MarkButton\n label={translate('builder.richText.link')}\n active={state !== null && state.linkHref !== null}\n onClick={openLinkEditor}\n >\n {translate('builder.richText.link')}\n </MarkButton>\n {state !== null && state.linkHref !== null ? (\n <MarkButton label={translate('builder.richText.removeLink')} active={false} onClick={() => exec('unlink')}>\n {translate('builder.richText.unlink')}\n </MarkButton>\n ) : null}\n </div>\n )}\n </div>,\n doc.body,\n )\n}\n\nfunction MarkButton({\n label,\n active,\n onClick,\n children,\n}: {\n label: string\n active: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={active}\n title={label}\n onClick={onClick}\n className=\"ec-rt-toolbar-btn\"\n data-active={active || undefined}\n >\n {children}\n </button>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"rich-text-toolbar.js","sourceRoot":"","sources":["../../../src/components/builder/rich-text-toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect, useRef, useState } from 'react'\nimport { createPortal } from 'react-dom'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Floating mark toolbar for inline rich-text editing (EC-159): anchored to the\n * live text selection inside the canvas iframe, offering bold, italic, and\n * link (set + remove). Portaled into the *iframe* document — selection\n * rectangles are iframe-viewport coordinates, so the toolbar positions\n * `fixed` in that viewport and never needs cross-frame math.\n *\n * In-frame chrome contract (EC-414 AD-B): the toolbar is styled EXCLUSIVELY by\n * the chrome stylesheet's `ec-rt-*` rules plus inline positioning — no studio\n * utilities, no studio UI primitives — and its root carries the `.ec-chrome`\n * containment recipe. The portal-free {@link RichTextToolbarView} is the\n * contract-testable markup surface.\n *\n * Mark application uses the document's native editing commands on the live\n * selection (`bold`/`italic`/`createLink`/`unlink`), which produce exactly the\n * tags the `@elytracms/core/rich-text` DOM parser inverts (`b/strong`, `i/em`,\n * `a[href]`). Pointer-down on the toolbar is swallowed so the host keeps its\n * selection; the link input is the one exception (it needs focus), and the\n * canvas blur handler ignores blurs into `[data-ec-rt-toolbar]`.\n */\n\ninterface ToolbarState {\n left: number\n top: number\n bold: boolean\n italic: boolean\n /** The href of the link the selection sits inside, if any. */\n linkHref: string | null\n}\n\nconst TOOLBAR_OFFSET_PX = 8\n\nfunction selectionState(doc: Document, host: HTMLElement): ToolbarState | null {\n const selection = doc.getSelection()\n if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return null\n if (!host.contains(selection.anchorNode) || !host.contains(selection.focusNode)) return null\n const rect = selection.getRangeAt(0).getBoundingClientRect()\n if (rect.width === 0 && rect.height === 0) return null\n const anchor = selection.anchorNode\n const anchorElement = anchor instanceof Element ? anchor : anchor?.parentElement\n const link = anchorElement?.closest('a') ?? null\n return {\n left: rect.left + rect.width / 2,\n top: rect.top,\n bold: doc.queryCommandState('bold'),\n italic: doc.queryCommandState('italic'),\n linkHref: link?.getAttribute('href') ?? null,\n }\n}\n\n/** Everything the portal-free toolbar markup needs (chrome contract surface). */\nexport interface RichTextToolbarViewProps {\n left: number\n top: number\n bold: boolean\n italic: boolean\n hasLink: boolean\n /** Non-null → the link editor row is open with this draft value. */\n linkDraft: string | null\n onToolbarMouseDown: (e: React.MouseEvent<HTMLDivElement>) => void\n onBold: () => void\n onItalic: () => void\n onOpenLink: () => void\n onUnlink: () => void\n onLinkDraftChange: (value: string) => void\n onLinkSubmit: () => void\n onLinkCancel: () => void\n}\n\n/**\n * The toolbar's rendered markup, portal-free so the AD-D chrome contract test\n * can walk it with `renderToStaticMarkup`: every class here MUST be `ec-*`.\n */\nexport function RichTextToolbarView(props: RichTextToolbarViewProps) {\n return (\n <div\n data-ec-rt-toolbar=\"\"\n role=\"toolbar\"\n aria-label={translate('builder.richText.toolbarLabel')}\n className=\"ec-chrome ec-rt-toolbar\"\n style={{\n position: 'fixed',\n left: Math.max(8, props.left),\n top: Math.max(8, props.top - TOOLBAR_OFFSET_PX),\n transform: 'translate(-50%, -100%)',\n zIndex: 50,\n }}\n onMouseDown={props.onToolbarMouseDown}\n >\n {props.linkDraft !== null ? (\n <form\n className=\"ec-rt-toolbar-row\"\n onSubmit={(e) => {\n e.preventDefault()\n props.onLinkSubmit()\n }}\n >\n <input\n autoFocus\n type=\"text\"\n value={props.linkDraft}\n placeholder={translate('builder.richText.linkPlaceholder')}\n aria-label={translate('builder.richText.linkUrl')}\n onChange={(e) => props.onLinkDraftChange(e.target.value)}\n onKeyDown={(e) => {\n if (e.key === 'Escape') {\n e.preventDefault()\n e.stopPropagation()\n props.onLinkCancel()\n }\n }}\n className=\"ec-rt-toolbar-input\"\n />\n <button type=\"submit\" className=\"ec-rt-toolbar-btn\" aria-label={translate('builder.richText.applyLink')}>\n {translate('builder.richText.set')}\n </button>\n </form>\n ) : (\n <div className=\"ec-rt-toolbar-row\">\n <MarkButton label={translate('builder.richText.bold')} active={props.bold} onClick={props.onBold}>\n B\n </MarkButton>\n <MarkButton label={translate('builder.richText.italic')} active={props.italic} onClick={props.onItalic}>\n I\n </MarkButton>\n <MarkButton label={translate('builder.richText.link')} active={props.hasLink} onClick={props.onOpenLink}>\n {translate('builder.richText.link')}\n </MarkButton>\n {props.hasLink ? (\n <MarkButton label={translate('builder.richText.removeLink')} active={false} onClick={props.onUnlink}>\n {translate('builder.richText.unlink')}\n </MarkButton>\n ) : null}\n </div>\n )}\n </div>\n )\n}\n\nexport function RichTextToolbar({ doc, host }: { doc: Document; host: HTMLElement }) {\n const [state, setState] = useState<ToolbarState | null>(null)\n const [linkOpen, setLinkOpen] = useState(false)\n const [linkDraft, setLinkDraft] = useState('')\n const savedRangeRef = useRef<Range | null>(null)\n\n const refresh = useCallback(() => {\n setState(selectionState(doc, host))\n }, [doc, host])\n\n useEffect(() => {\n refresh()\n doc.addEventListener('selectionchange', refresh)\n return () => doc.removeEventListener('selectionchange', refresh)\n }, [doc, refresh])\n\n // Close a stale link editor when the selection leaves the host entirely.\n useEffect(() => {\n if (!state && !linkOpen) setLinkDraft('')\n }, [state, linkOpen])\n\n const exec = useCallback(\n (command: string, value?: string) => {\n doc.execCommand(command, false, value)\n refresh()\n },\n [doc, refresh],\n )\n\n const openLinkEditor = useCallback(() => {\n const selection = doc.getSelection()\n if (selection && selection.rangeCount > 0) {\n savedRangeRef.current = selection.getRangeAt(0).cloneRange()\n }\n setLinkDraft(state?.linkHref ?? '')\n setLinkOpen(true)\n }, [doc, state])\n\n const applyLink = useCallback(\n (href: string) => {\n const selection = doc.getSelection()\n const saved = savedRangeRef.current\n if (saved && selection) {\n selection.removeAllRanges()\n selection.addRange(saved)\n }\n const trimmed = href.trim()\n if (trimmed.length > 0) exec('createLink', trimmed)\n else exec('unlink')\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n },\n [doc, exec, host],\n )\n\n const closeLinkEditor = useCallback(() => {\n setLinkOpen(false)\n savedRangeRef.current = null\n host.focus({ preventScroll: true })\n }, [host])\n\n if (!state && !linkOpen) return null\n const left = state?.left ?? savedRangeRef.current?.getBoundingClientRect().left ?? 0\n const top = state?.top ?? savedRangeRef.current?.getBoundingClientRect().top ?? 0\n\n return createPortal(\n <RichTextToolbarView\n left={left}\n top={top}\n bold={state?.bold ?? false}\n italic={state?.italic ?? false}\n hasLink={state !== null && state.linkHref !== null}\n linkDraft={linkOpen ? linkDraft : null}\n onToolbarMouseDown={(e) => {\n // Keep the host's selection/focus — except for the link input.\n if (!(e.target as HTMLElement).closest('input')) e.preventDefault()\n }}\n onBold={() => exec('bold')}\n onItalic={() => exec('italic')}\n onOpenLink={openLinkEditor}\n onUnlink={() => exec('unlink')}\n onLinkDraftChange={setLinkDraft}\n onLinkSubmit={() => applyLink(linkDraft)}\n onLinkCancel={closeLinkEditor}\n />,\n doc.body,\n )\n}\n\nfunction MarkButton({\n label,\n active,\n onClick,\n children,\n}: {\n label: string\n active: boolean\n onClick: () => void\n children: React.ReactNode\n}) {\n return (\n <button\n type=\"button\"\n aria-label={label}\n aria-pressed={active}\n title={label}\n onClick={onClick}\n className=\"ec-rt-toolbar-btn\"\n data-active={active || undefined}\n >\n {children}\n </button>\n )\n}\n"]}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { InsertPosition } from './insert-context.js';
|
|
2
|
+
import type { AffordanceAnchor, FrameBarPlacement } from './canvas-overlay-geometry.js';
|
|
3
|
+
import type { Rect } from './canvas-rect-tracking.js';
|
|
4
|
+
/**
|
|
5
|
+
* Canvas selection/hover overlay (EC-404) — a topmost layer portaled into the
|
|
6
|
+
* iframe `body` that draws the selection and hover frames ABOVE the page from
|
|
7
|
+
* measured node rects, so a full-bleed background or a higher-`z-index` sibling
|
|
8
|
+
* can never paint over them (the structural fix for the in-document inset
|
|
9
|
+
* outline the findings batch flagged). The selected frame carries a compact tag:
|
|
10
|
+
* the component's display name plus move-up / move-down / duplicate / delete
|
|
11
|
+
* controls that dispatch the EXISTING tree actions.
|
|
12
|
+
*
|
|
13
|
+
* EC-414 AD-B chrome contract: everything here is `ec-*` classes / `data-ec-*`
|
|
14
|
+
* attributes / inline geometry only, styled by `canvas-chrome-css.ts`, with the
|
|
15
|
+
* `.ec-chrome` containment recipe on the root; no studio Tailwind utilities and
|
|
16
|
+
* no studio React UI primitives cross into the iframe. The rect-tracking core
|
|
17
|
+
* ({@link createRectTracker}) is a pure, reusable module — EC-405 rides this
|
|
18
|
+
* overlay layer next, and EC-406 can share the tracking.
|
|
19
|
+
*/
|
|
20
|
+
/** The overlay's full measured geometry (both frames + the bar placement). */
|
|
21
|
+
export interface OverlayGeometry {
|
|
22
|
+
selected: Rect | null;
|
|
23
|
+
/** Chrome-bar placement for the selected frame (null when nothing to draw). */
|
|
24
|
+
selectedBar: FrameBarPlacement | null;
|
|
25
|
+
hovered: Rect | null;
|
|
26
|
+
/** EC-408: the focused prop's marker rect (reverse highlight), or null. */
|
|
27
|
+
propHighlight: Rect | null;
|
|
28
|
+
}
|
|
29
|
+
/** A drawn selection frame plus its control enablement. */
|
|
30
|
+
export interface SelectionFrameView {
|
|
31
|
+
rect: Rect;
|
|
32
|
+
bar: FrameBarPlacement;
|
|
33
|
+
label: string;
|
|
34
|
+
canMoveUp: boolean;
|
|
35
|
+
canMoveDown: boolean;
|
|
36
|
+
}
|
|
37
|
+
/** One "+" affordance button: its position and its placed anchor point. */
|
|
38
|
+
export interface AffordanceItemView extends AffordanceAnchor {
|
|
39
|
+
position: InsertPosition;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The affordance buttons for one node — grouped so a single `onInsert` binds the
|
|
43
|
+
* node id the inserts target (the selected node and, separately, a hovered one).
|
|
44
|
+
*/
|
|
45
|
+
export interface AffordanceGroupView {
|
|
46
|
+
nodeId: string;
|
|
47
|
+
items: AffordanceItemView[];
|
|
48
|
+
onInsert: (position: InsertPosition) => void;
|
|
49
|
+
}
|
|
50
|
+
export interface SelectionOverlayViewProps {
|
|
51
|
+
selected: SelectionFrameView | null;
|
|
52
|
+
hovered: Rect | null;
|
|
53
|
+
/** In-canvas insertion affordances (EC-405); empty when suppressed (drag/editing). */
|
|
54
|
+
affordances: AffordanceGroupView[];
|
|
55
|
+
/** EC-408: the focused prop's marker rect (reverse highlight), or null. */
|
|
56
|
+
propHighlight?: Rect | null;
|
|
57
|
+
onMoveUp: () => void;
|
|
58
|
+
onMoveDown: () => void;
|
|
59
|
+
onDuplicate: () => void;
|
|
60
|
+
onDelete: () => void;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The overlay's rendered markup, portal-free so the AD-D chrome contract test
|
|
64
|
+
* can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root
|
|
65
|
+
* carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).
|
|
66
|
+
*/
|
|
67
|
+
export declare function SelectionOverlayView(props: SelectionOverlayViewProps): import("react").JSX.Element;
|
|
68
|
+
/**
|
|
69
|
+
* The live overlay: tracks the selected + hovered node rects (glued through
|
|
70
|
+
* scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing
|
|
71
|
+
* {@link createRectTracker}) and portals the frames into the iframe `body`.
|
|
72
|
+
* Suppressed for the composition root (no frame around the whole page — mirrors
|
|
73
|
+
* the tree, which offers the root no actions) and while an inline text/rich-text
|
|
74
|
+
* edit is live (the `[data-ec-editing]` outline owns that state).
|
|
75
|
+
*/
|
|
76
|
+
export declare function SelectionOverlay({ doc, editing }: {
|
|
77
|
+
doc: Document;
|
|
78
|
+
editing: boolean;
|
|
79
|
+
}): import("react").ReactPortal;
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { createPortal } from 'react-dom';
|
|
4
|
+
import { getNode, getParentInfo } from '@elytracms/core/project-graph';
|
|
5
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
6
|
+
import { translate, useUiLocale } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { containerFlowAxis } from '@elytracms/studio/lib/dnd/drop-rules';
|
|
8
|
+
import { useBuilder } from './builder-store.js';
|
|
9
|
+
import { activeDrag, onDragSessionEnd, onDragSessionStart } from './drag-session.js';
|
|
10
|
+
import { useInsertAffordance } from './insert-affordance-context.js';
|
|
11
|
+
import { FRAME_BAR_HEIGHT, frameControlAction, frameControlsFor, placeAffordance, placeFrameBar, } from './canvas-overlay-geometry.js';
|
|
12
|
+
import { createRectTracker, measureNodeRect, measurePropRect, rectsEqual, scheduleFrame, subscribeCanvasLayout, } from './canvas-rect-tracking.js';
|
|
13
|
+
import { usePropSync } from './prop-sync-context.js';
|
|
14
|
+
import { nodeDisplayTitle } from './node-title.js';
|
|
15
|
+
const EMPTY_GEOMETRY = {
|
|
16
|
+
selected: null,
|
|
17
|
+
selectedBar: null,
|
|
18
|
+
hovered: null,
|
|
19
|
+
propHighlight: null,
|
|
20
|
+
};
|
|
21
|
+
function barsEqual(a, b) {
|
|
22
|
+
if (a === null || b === null)
|
|
23
|
+
return a === b;
|
|
24
|
+
return a.top === b.top && a.left === b.left && a.placement === b.placement;
|
|
25
|
+
}
|
|
26
|
+
function geometryEquals(a, b) {
|
|
27
|
+
return (rectsEqual(a.selected, b.selected) &&
|
|
28
|
+
rectsEqual(a.hovered, b.hovered) &&
|
|
29
|
+
rectsEqual(a.propHighlight, b.propHighlight) &&
|
|
30
|
+
barsEqual(a.selectedBar, b.selectedBar));
|
|
31
|
+
}
|
|
32
|
+
function frameStyle(rect) {
|
|
33
|
+
return { left: rect.left, top: rect.top, width: rect.width, height: rect.height };
|
|
34
|
+
}
|
|
35
|
+
const AFFORDANCE_LABELS = {
|
|
36
|
+
before: 'builder.canvas.insertBefore',
|
|
37
|
+
after: 'builder.canvas.insertAfter',
|
|
38
|
+
into: 'builder.canvas.insertInto',
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The overlay's rendered markup, portal-free so the AD-D chrome contract test
|
|
42
|
+
* can walk it with `renderToStaticMarkup`: every class is `ec-*`, the root
|
|
43
|
+
* carries the `.ec-chrome` containment recipe, geometry is inline (no `var()`).
|
|
44
|
+
*/
|
|
45
|
+
export function SelectionOverlayView(props) {
|
|
46
|
+
const { selected, hovered, affordances, propHighlight } = props;
|
|
47
|
+
return (_jsxs("div", { className: "ec-chrome ec-overlay", "data-ec-overlay": "", children: [propHighlight ? (_jsx("div", { className: "ec-overlay-prop", "data-ec-prop-highlight": "", style: frameStyle(propHighlight) })) : null, hovered ? _jsx("div", { className: "ec-overlay-frame", "data-ec-hover": "", style: frameStyle(hovered) }) : null, affordances.map((group) => group.items.map((item) => (_jsx(AddButton, { item: item, onInsert: () => group.onInsert(item.position) }, `${group.nodeId}:${item.position}`)))), selected ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "ec-overlay-frame", "data-ec-selected": "", style: frameStyle(selected.rect) }), _jsxs("div", { className: "ec-overlay-bar", "data-ec-frame-bar": "", "data-ec-placement": selected.bar.placement, style: { left: selected.bar.left, top: selected.bar.top }, children: [_jsx("span", { className: "ec-overlay-label", title: selected.label, children: selected.label }), _jsx("span", { className: "ec-overlay-sep", "aria-hidden": "true" }), _jsx(FrameButton, { label: translate('builder.pageTree.moveUp'), disabled: !selected.canMoveUp, onClick: props.onMoveUp, children: _jsx(OverlayIcon, { kind: "moveUp" }) }), _jsx(FrameButton, { label: translate('builder.pageTree.moveDown'), disabled: !selected.canMoveDown, onClick: props.onMoveDown, children: _jsx(OverlayIcon, { kind: "moveDown" }) }), _jsx(FrameButton, { label: translate('common.duplicate'), onClick: props.onDuplicate, children: _jsx(OverlayIcon, { kind: "duplicate" }) }), _jsx(FrameButton, { label: translate('common.delete'), onClick: props.onDelete, children: _jsx(OverlayIcon, { kind: "delete" }) })] })] })) : null] }));
|
|
48
|
+
}
|
|
49
|
+
function FrameButton({ label, disabled, onClick, children, }) {
|
|
50
|
+
return (_jsx("button", { type: "button", className: "ec-overlay-btn", "aria-label": label, title: label, disabled: disabled,
|
|
51
|
+
// Keep the click on the control: it must not fall through to the canvas
|
|
52
|
+
// selection listener (the bar sits over the page) nor start a text drag.
|
|
53
|
+
onMouseDown: (e) => e.preventDefault(), onClick: (e) => {
|
|
54
|
+
e.preventDefault();
|
|
55
|
+
e.stopPropagation();
|
|
56
|
+
onClick();
|
|
57
|
+
}, children: children }));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A "+" insertion affordance (EC-405): a small round button centered on a block
|
|
61
|
+
* boundary (before/after edge, or the center of an empty container). Centered on
|
|
62
|
+
* its anchor by the chrome CSS `translate(-50%,-50%)`; `pointer-events` re-enabled
|
|
63
|
+
* on the button while the overlay root stays click-through. The gesture guards
|
|
64
|
+
* mirror `FrameButton` — the press must not fall through to the canvas selection
|
|
65
|
+
* listener nor start a native drag.
|
|
66
|
+
*/
|
|
67
|
+
function AddButton({ item, onInsert }) {
|
|
68
|
+
const label = translate(AFFORDANCE_LABELS[item.position]);
|
|
69
|
+
return (_jsx("button", { type: "button", className: "ec-overlay-add", "data-ec-affordance": item.position, "aria-label": label, title: label, style: { left: item.left, top: item.top }, onMouseDown: (e) => e.preventDefault(), onClick: (e) => {
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
onInsert();
|
|
73
|
+
}, children: _jsx("svg", { width: 14, height: 14, viewBox: "0 0 16 16", fill: "none", stroke: "currentColor", strokeWidth: 1.8, strokeLinecap: "round", "aria-hidden": "true", children: _jsx("path", { d: "M8 3.5v9M3.5 8h9" }) }) }));
|
|
74
|
+
}
|
|
75
|
+
/** Hand-rolled inline icons — no `class` attribute (chrome contract). */
|
|
76
|
+
function OverlayIcon({ kind }) {
|
|
77
|
+
const common = {
|
|
78
|
+
width: 13,
|
|
79
|
+
height: 13,
|
|
80
|
+
viewBox: '0 0 16 16',
|
|
81
|
+
fill: 'none',
|
|
82
|
+
stroke: 'currentColor',
|
|
83
|
+
strokeWidth: 1.6,
|
|
84
|
+
strokeLinecap: 'round',
|
|
85
|
+
strokeLinejoin: 'round',
|
|
86
|
+
'aria-hidden': true,
|
|
87
|
+
};
|
|
88
|
+
switch (kind) {
|
|
89
|
+
case 'moveUp':
|
|
90
|
+
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M4 10l4-4 4 4" }) }));
|
|
91
|
+
case 'moveDown':
|
|
92
|
+
return (_jsx("svg", { ...common, children: _jsx("path", { d: "M4 6l4 4 4-4" }) }));
|
|
93
|
+
case 'duplicate':
|
|
94
|
+
return (_jsxs("svg", { ...common, children: [_jsx("rect", { x: "6", y: "6", width: "8", height: "8", rx: "1.5" }), _jsx("path", { d: "M10 6V4a1 1 0 0 0-1-1H3a1 1 0 0 0-1 1v6a1 1 0 0 0 1 1h2" })] }));
|
|
95
|
+
case 'delete':
|
|
96
|
+
return (_jsxs("svg", { ...common, children: [_jsx("path", { d: "M3 4.5h10" }), _jsx("path", { d: "M6.5 4.5V3a1 1 0 0 1 1-1h1a1 1 0 0 1 1 1v1.5" }), _jsx("path", { d: "M5 4.5l.5 8a1 1 0 0 0 1 .9h3a1 1 0 0 0 1-.9l.5-8" })] }));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The live overlay: tracks the selected + hovered node rects (glued through
|
|
101
|
+
* scroll/resize/async layout via {@link subscribeCanvasLayout} + a coalescing
|
|
102
|
+
* {@link createRectTracker}) and portals the frames into the iframe `body`.
|
|
103
|
+
* Suppressed for the composition root (no frame around the whole page — mirrors
|
|
104
|
+
* the tree, which offers the root no actions) and while an inline text/rich-text
|
|
105
|
+
* edit is live (the `[data-ec-editing]` outline owns that state).
|
|
106
|
+
*/
|
|
107
|
+
export function SelectionOverlay({ doc, editing }) {
|
|
108
|
+
const builder = useBuilder();
|
|
109
|
+
const { present, selectedId, hoveredId, dispatch } = builder;
|
|
110
|
+
const studio = getStudioRegistry();
|
|
111
|
+
const affordance = useInsertAffordance();
|
|
112
|
+
const propSync = usePropSync();
|
|
113
|
+
const uiLocale = useUiLocale();
|
|
114
|
+
const [geometry, setGeometry] = useState(EMPTY_GEOMETRY);
|
|
115
|
+
// EC-408 reverse highlight: the focused prop's marker, but only for the
|
|
116
|
+
// selected node (the inspector only shows that node's props) and never while
|
|
117
|
+
// inline-editing (the [data-ec-editing] outline owns the node then).
|
|
118
|
+
const highlight = propSync?.highlighted && propSync.highlighted.nodeId === selectedId && !editing
|
|
119
|
+
? propSync.highlighted
|
|
120
|
+
: null;
|
|
121
|
+
const highlightNodeId = highlight?.nodeId ?? null;
|
|
122
|
+
const highlightPropName = highlight?.propName ?? null;
|
|
123
|
+
// EC-405 scope 4: insertion affordances hide while ANY drag is live (the
|
|
124
|
+
// DnD before/after/into indicators own the "where it lands" story then).
|
|
125
|
+
const [dragging, setDragging] = useState(() => activeDrag() !== null);
|
|
126
|
+
useEffect(() => {
|
|
127
|
+
const offStart = onDragSessionStart(() => setDragging(true));
|
|
128
|
+
const offEnd = onDragSessionEnd(() => setDragging(false));
|
|
129
|
+
return () => {
|
|
130
|
+
offStart();
|
|
131
|
+
offEnd();
|
|
132
|
+
};
|
|
133
|
+
}, []);
|
|
134
|
+
// The selected node's controls/label come from the graph; the root and a
|
|
135
|
+
// never-found selection draw nothing. Hover never frames the currently
|
|
136
|
+
// selected node (one frame at a time) nor the root.
|
|
137
|
+
const selectedNode = selectedId && !editing ? getNode(present, selectedId) : null;
|
|
138
|
+
const controls = selectedNode ? frameControlsFor(present, selectedId) : null;
|
|
139
|
+
const drawSelected = selectedNode !== null && controls !== null && !controls.isRoot;
|
|
140
|
+
const hoveredNode = hoveredId && hoveredId !== selectedId ? getNode(present, hoveredId) : null;
|
|
141
|
+
const drawHover = hoveredNode !== null && !frameControlsFor(present, hoveredId).isRoot;
|
|
142
|
+
useLayoutEffect(() => {
|
|
143
|
+
const win = doc.defaultView;
|
|
144
|
+
if (!win)
|
|
145
|
+
return;
|
|
146
|
+
const selId = drawSelected ? selectedId : null;
|
|
147
|
+
const hovId = drawHover ? hoveredId : null;
|
|
148
|
+
const measure = () => {
|
|
149
|
+
const selected = measureNodeRect(doc, selId);
|
|
150
|
+
const hovered = measureNodeRect(doc, hovId);
|
|
151
|
+
return {
|
|
152
|
+
selected,
|
|
153
|
+
selectedBar: selected ? placeFrameBar(selected, { barHeight: FRAME_BAR_HEIGHT, docTop: win.scrollY }) : null,
|
|
154
|
+
hovered,
|
|
155
|
+
propHighlight: measurePropRect(doc, highlightNodeId, highlightPropName),
|
|
156
|
+
};
|
|
157
|
+
};
|
|
158
|
+
const targets = () => {
|
|
159
|
+
const out = [];
|
|
160
|
+
for (const id of [selId, hovId]) {
|
|
161
|
+
if (!id)
|
|
162
|
+
continue;
|
|
163
|
+
const el = doc.querySelector(`[data-ec-node-id="${id}"]`);
|
|
164
|
+
if (el)
|
|
165
|
+
out.push(el);
|
|
166
|
+
}
|
|
167
|
+
// Track the highlighted marker element too, so it re-glues on async layout.
|
|
168
|
+
if (highlightNodeId) {
|
|
169
|
+
const wrapper = doc.querySelector(`[data-ec-node-id="${highlightNodeId}"]`);
|
|
170
|
+
if (wrapper)
|
|
171
|
+
out.push(wrapper);
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
};
|
|
175
|
+
// Coalesce on the PARENT window's frame loop (not the iframe's): this
|
|
176
|
+
// component + its state live in the parent React tree, and the iframe's rAF
|
|
177
|
+
// is throttled/paused whenever the preview is offscreen or backgrounded. The
|
|
178
|
+
// compound rAF+timeout scheduler (scheduleFrame) keeps the flush paint-
|
|
179
|
+
// aligned on screen while guaranteeing it still runs when rAF is paused.
|
|
180
|
+
const host = win.parent ?? window;
|
|
181
|
+
const tracker = createRectTracker({
|
|
182
|
+
measure,
|
|
183
|
+
equals: geometryEquals,
|
|
184
|
+
onChange: setGeometry,
|
|
185
|
+
schedule: (flush) => scheduleFrame(host, flush),
|
|
186
|
+
subscribe: (invalidate) => subscribeCanvasLayout(doc, targets, invalidate),
|
|
187
|
+
});
|
|
188
|
+
return () => tracker.dispose();
|
|
189
|
+
// Re-subscribe when the tracked ids / graph / highlighted prop change so the
|
|
190
|
+
// ResizeObserver re-points at the current (possibly remounted) wrappers and
|
|
191
|
+
// the measure closure reads the current highlight target.
|
|
192
|
+
}, [doc, drawSelected, drawHover, selectedId, hoveredId, present, highlightNodeId, highlightPropName]);
|
|
193
|
+
// EC-407: the frame label shares the tree row's resolution — the SAME
|
|
194
|
+
// nodeDisplayTitle.primary the selected row shows (instance title when
|
|
195
|
+
// derivable, else the localized component name).
|
|
196
|
+
const label = selectedNode
|
|
197
|
+
? nodeDisplayTitle(studio.registry.getManifest(selectedNode.componentId), selectedNode, uiLocale).primary
|
|
198
|
+
: '';
|
|
199
|
+
const selectedView = drawSelected && geometry.selected && geometry.selectedBar && controls
|
|
200
|
+
? {
|
|
201
|
+
rect: geometry.selected,
|
|
202
|
+
bar: geometry.selectedBar,
|
|
203
|
+
label,
|
|
204
|
+
canMoveUp: controls.canMoveUp,
|
|
205
|
+
canMoveDown: controls.canMoveDown,
|
|
206
|
+
}
|
|
207
|
+
: null;
|
|
208
|
+
const run = (kind) => {
|
|
209
|
+
if (!selectedId)
|
|
210
|
+
return;
|
|
211
|
+
dispatch(frameControlAction(kind, selectedId));
|
|
212
|
+
};
|
|
213
|
+
// EC-405: the "+" affordances for the selected node and (separately) a hovered
|
|
214
|
+
// one — computed from the SAME measured rects the frames use. A group is drawn
|
|
215
|
+
// only for a node with insertable positions, and only when no drag/inline edit
|
|
216
|
+
// is live; a node with no provider above it (component preview) shows none.
|
|
217
|
+
const affordanceGroups = affordance && !dragging && !editing
|
|
218
|
+
? [
|
|
219
|
+
affordanceGroup(present, selectedId, drawSelected ? geometry.selected : null, affordance),
|
|
220
|
+
affordanceGroup(present, hoveredId, drawHover ? geometry.hovered : null, affordance),
|
|
221
|
+
].filter((g) => g !== null)
|
|
222
|
+
: [];
|
|
223
|
+
return createPortal(_jsx(SelectionOverlayView, { selected: selectedView, hovered: drawHover ? geometry.hovered : null, affordances: affordanceGroups, propHighlight: highlight ? geometry.propHighlight : null, onMoveUp: () => run('moveUp'), onMoveDown: () => run('moveDown'), onDuplicate: () => run('duplicate'), onDelete: () => run('delete') }), doc.body);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Build the affordance group for one node from its measured rect: the insertable
|
|
227
|
+
* positions the provider allows (slot rules already applied), each placed on the
|
|
228
|
+
* node's boundary along the parent's flow axis. Returns `null` when there is no
|
|
229
|
+
* rect yet or no insertable position.
|
|
230
|
+
*/
|
|
231
|
+
function affordanceGroup(present, nodeId, rect, affordance) {
|
|
232
|
+
if (!nodeId || !rect)
|
|
233
|
+
return null;
|
|
234
|
+
const positions = affordance.positionsFor(nodeId);
|
|
235
|
+
if (positions.length === 0)
|
|
236
|
+
return null;
|
|
237
|
+
const flow = containerFlowAxis(getParentInfo(present, nodeId)?.parent);
|
|
238
|
+
return {
|
|
239
|
+
nodeId,
|
|
240
|
+
items: positions.map((position) => ({ position, ...placeAffordance(rect, position, flow) })),
|
|
241
|
+
onInsert: (position) => affordance.openAt(nodeId, position),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=selection-overlay.js.map
|