@elytracms/studio 0.0.35 → 0.0.37
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/dist/components/assets/asset-selection.d.ts +1 -1
- package/dist/components/assets/asset-selection.js +2 -2
- package/dist/components/assets/asset-selection.js.map +1 -1
- package/dist/components/builder/clipboard-actions.js +1 -1
- package/dist/components/builder/clipboard-actions.js.map +1 -1
- package/dist/components/builder/composition-field-editor.js +7 -7
- package/dist/components/builder/composition-field-editor.js.map +1 -1
- package/dist/components/builder/inline-edit.js +4 -3
- package/dist/components/builder/inline-edit.js.map +1 -1
- package/dist/components/builder/insert-context.js +1 -1
- package/dist/components/builder/insert-context.js.map +1 -1
- package/dist/components/builder/layout-inspector.js +2 -2
- package/dist/components/builder/layout-inspector.js.map +1 -1
- package/dist/components/builder/node-factory.js +7 -7
- package/dist/components/builder/node-factory.js.map +1 -1
- package/dist/components/builder/page-tree.js +1 -1
- package/dist/components/builder/page-tree.js.map +1 -1
- package/dist/components/builder/property-inspector.js +2 -2
- package/dist/components/builder/property-inspector.js.map +1 -1
- package/dist/components/builder/reuse-context.js +2 -2
- package/dist/components/builder/reuse-context.js.map +1 -1
- package/dist/components/builder/reuse.js +5 -4
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/cell.js +1 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/editable-cell.js +4 -4
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +11 -10
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/relation-picker.d.ts +2 -2
- package/dist/components/cms/relation-picker.js +5 -5
- package/dist/components/cms/relation-picker.js.map +1 -1
- package/dist/components/cms/rich-text/asset-image.d.ts +12 -0
- package/dist/components/cms/rich-text/asset-image.js +24 -0
- package/dist/components/cms/rich-text/asset-image.js.map +1 -0
- package/dist/components/cms/rich-text/component-embed.d.ts +15 -0
- package/dist/components/cms/rich-text/component-embed.js +54 -0
- package/dist/components/cms/rich-text/component-embed.js.map +1 -0
- package/dist/components/cms/rich-text/editor.d.ts +24 -0
- package/dist/components/cms/rich-text/editor.js +93 -0
- package/dist/components/cms/rich-text/editor.js.map +1 -0
- package/dist/components/cms/rich-text/embed-control.d.ts +17 -0
- package/dist/components/cms/rich-text/embed-control.js +62 -0
- package/dist/components/cms/rich-text/embed-control.js.map +1 -0
- package/dist/components/cms/rich-text/extensions.d.ts +23 -0
- package/dist/components/cms/rich-text/extensions.js +40 -0
- package/dist/components/cms/rich-text/extensions.js.map +1 -0
- package/dist/components/cms/rich-text/image-control.d.ts +16 -0
- package/dist/components/cms/rich-text/image-control.js +36 -0
- package/dist/components/cms/rich-text/image-control.js.map +1 -0
- package/dist/components/cms/rich-text/index.d.ts +7 -0
- package/dist/components/cms/rich-text/index.js +8 -0
- package/dist/components/cms/rich-text/index.js.map +1 -0
- package/dist/components/cms/rich-text/link-control.d.ts +22 -0
- package/dist/components/cms/rich-text/link-control.js +65 -0
- package/dist/components/cms/rich-text/link-control.js.map +1 -0
- package/dist/components/cms/rich-text/ref-link.d.ts +14 -0
- package/dist/components/cms/rich-text/ref-link.js +26 -0
- package/dist/components/cms/rich-text/ref-link.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar-button.d.ts +15 -0
- package/dist/components/cms/rich-text/toolbar-button.js +11 -0
- package/dist/components/cms/rich-text/toolbar-button.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.d.ts +20 -0
- package/dist/components/cms/rich-text/toolbar.js +40 -0
- package/dist/components/cms/rich-text/toolbar.js.map +1 -0
- package/dist/components/cms/rich-text/value.d.ts +33 -0
- package/dist/components/cms/rich-text/value.js +114 -0
- package/dist/components/cms/rich-text/value.js.map +1 -0
- package/dist/components/components/component-detail.js +1 -1
- package/dist/components/components/component-detail.js.map +1 -1
- package/dist/components/components/components-index.js +1 -1
- package/dist/components/components/components-index.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/extensions/extensions-view.js +1 -1
- package/dist/components/extensions/extensions-view.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +1 -1
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.js +2 -2
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/dnd/gap-drag.js +2 -2
- package/dist/lib/dnd/gap-drag.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +33 -0
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persona/labels.d.ts +2 -2
- package/dist/lib/persona/labels.js +3 -3
- package/dist/lib/persona/labels.js.map +1 -1
- package/dist/lib/references/reference-model.js +1 -1
- package/dist/lib/references/reference-model.js.map +1 -1
- package/dist/lib/registry/preview.js +15 -15
- package/dist/lib/registry/preview.js.map +1 -1
- package/dist/lib/registry/project-components.js +6 -13
- package/dist/lib/registry/project-components.js.map +1 -1
- package/package.json +8 -2
- package/src/styles/studio.css +105 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"relation-picker.js","sourceRoot":"","sources":["../../../src/components/cms/relation-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Check, Search, X } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Badge,\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/** Pick related document(s) from a target collection (brief §8.4, EC-074). */\nexport function RelationPicker({\n targetCollection,\n cardinality,\n value,\n open,\n onOpenChange,\n onChange,\n excludeIds,\n}: {\n targetCollection: string\n cardinality: 'one' | 'many'\n value: DocumentRef[]\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (refs: DocumentRef[]) => void\n /**\n * Document ids to omit from the candidate list (EC-218): a `parent` self-relation\n * passes the editing document plus its descendants, so a page can never be set\n * as its own ancestor (which would form a cycle).\n */\n excludeIds?: ReadonlySet<string>\n}) {\n const source = useCollectionView()\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState<DocumentRef[]>(value)\n\n // Seed selection only on open transitions — `value` is a fresh array each\n // parent render, so depending on it would reset in-progress multi-selection.\n useEffect(() => {\n if (open) {\n setSelected(value)\n setQuery('')\n }\n }, [open])\n\n const candidates = source.referenceCandidates(targetCollection).filter((d) => {\n if (excludeIds?.has(d.id)) return false\n if (!query.trim()) return true\n return source.titleOf({ collection: targetCollection, id: d.id }).toLowerCase().includes(query.toLowerCase())\n })\n\n const isSelected = (id: string) => selected.some((r) => r.id === id)\n\n function toggle(id: string) {\n const ref: DocumentRef = { collection: targetCollection, id }\n if (cardinality === 'one') {\n setSelected([ref])\n onChange([ref])\n onOpenChange(false)\n return\n }\n setSelected((prev) => (isSelected(id) ? prev.filter((r) => r.id !== id) : [...prev, ref]))\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate(cardinality === 'many' ? 'cms.relation.pickTitle.many' : 'cms.relation.pickTitle.one')}\n </DialogTitle>\n <DialogDescription>\n From <Code tone=\"muted\">{targetCollection}</Code>\n {cardinality === 'many' ? ` · ${translate('cms.relation.selectMultipleHint')}` : ''}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n\n <div className=\"max-h-72 overflow-auto\">\n {candidates.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {translate('cms.relation.noDocuments', { collection: targetCollection })}\n </Text>\n ) : (\n candidates.map((doc) => {\n const title = source.titleOf({ collection: targetCollection, id: doc.id })\n const sel = isSelected(doc.id)\n return (\n <button\n key={doc.id}\n type=\"button\"\n onClick={() => toggle(doc.id)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted',\n sel && 'bg-accent text-accent-foreground',\n )}\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{title}</span>\n <Code tone=\"muted\">{doc.id}</Code>\n {sel ? <Check className=\"size-4 text-primary\" /> : null}\n </button>\n )\n })\n )}\n </div>\n\n {value.length > 0 || cardinality === 'many' ? (\n <DialogFooter>\n <Row gap=\"2\" className=\"w-full\">\n {/* EC-246c: a way to CLEAR the relation from the picker — the only\n remove path for a cardinality-`one` relation edited inline in the\n list (the form field has its own per-row X). */}\n {value.length > 0 ? (\n <Button\n variant=\"ghost\"\n className=\"text-destructive hover:text-destructive\"\n data-testid=\"relation-remove\"\n onClick={() => {\n onChange([])\n onOpenChange(false)\n }}\n >\n <X /> {translate('common.remove')}\n </Button>\n ) : null}\n <Spacer />\n {cardinality === 'many' ? (\n <>\n <Badge variant=\"outline\">{translate('cms.relation.selectedCount', { count: selected.length })}</Badge>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n {translate('common.cancel')}\n </Button>\n <Button\n onClick={() => {\n onChange(selected)\n onOpenChange(false)\n }}\n >\n {translate('common.apply')}\n </Button>\n </>\n ) : null}\n </Row>\n </DialogFooter>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"relation-picker.js","sourceRoot":"","sources":["../../../src/components/cms/relation-picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,cAAc,CAAA;AAC/C,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAE3C,OAAO,EACL,KAAK,EACL,MAAM,EACN,IAAI,EACJ,MAAM,EACN,aAAa,EACb,iBAAiB,EACjB,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,KAAK,EACL,GAAG,EACH,MAAM,EACN,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,iBAAiB,EAAE,MAAM,4CAA4C,CAAA;AAE9E,8EAA8E;AAC9E,MAAM,UAAU,cAAc,CAAC,EAC7B,gBAAgB,EAChB,QAAQ,EACR,KAAK,EACL,IAAI,EACJ,YAAY,EACZ,QAAQ,EACR,UAAU,GAcX;IACC,MAAM,MAAM,GAAG,iBAAiB,EAAE,CAAA;IAClC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IACtC,MAAM,CAAC,QAAQ,EAAE,WAAW,CAAC,GAAG,QAAQ,CAAgB,KAAK,CAAC,CAAA;IAE9D,0EAA0E;IAC1E,6EAA6E;IAC7E,SAAS,CAAC,GAAG,EAAE;QACb,IAAI,IAAI,EAAE,CAAC;YACT,WAAW,CAAC,KAAK,CAAC,CAAA;YAClB,QAAQ,CAAC,EAAE,CAAC,CAAA;QACd,CAAC;IACH,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAA;IAEV,MAAM,UAAU,GAAG,MAAM,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE;QAC3E,IAAI,UAAU,EAAE,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAAE,OAAO,KAAK,CAAA;QACvC,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE;YAAE,OAAO,IAAI,CAAA;QAC9B,OAAO,MAAM,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAA;IAC/G,CAAC,CAAC,CAAA;IAEF,MAAM,UAAU,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAA;IAEpE,SAAS,MAAM,CAAC,EAAU;QACxB,MAAM,GAAG,GAAgB,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,CAAA;QAC7D,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,WAAW,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YAClB,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;YACf,YAAY,CAAC,KAAK,CAAC,CAAA;YACnB,OAAM;QACR,CAAC;QACD,WAAW,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,CAAA;IAC5F,CAAC;IAED,OAAO,CACL,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,YAC5C,MAAC,aAAa,IAAC,SAAS,EAAC,UAAU,aACjC,MAAC,YAAY,eACX,KAAC,WAAW,cACT,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,6BAA6B,CAAC,CAAC,CAAC,4BAA4B,CAAC,GACvE,EACd,MAAC,iBAAiB,wBACX,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,gBAAgB,GAAQ,EAChD,QAAQ,CAAC,CAAC,CAAC,MAAM,SAAS,CAAC,iCAAiC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IACnD,IACP,EAEf,eAAK,SAAS,EAAC,UAAU,aACvB,KAAC,MAAM,IAAC,SAAS,EAAC,+FAA+F,GAAG,EACpH,KAAC,KAAK,IACJ,SAAS,QACT,KAAK,EAAE,KAAK,EACZ,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACzC,WAAW,EAAE,SAAS,CAAC,0BAA0B,CAAC,EAClD,SAAS,EAAC,MAAM,GAChB,IACE,EAEN,cAAK,SAAS,EAAC,wBAAwB,YACpC,UAAU,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACzB,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,KAAK,YACzC,SAAS,CAAC,0BAA0B,EAAE,EAAE,UAAU,EAAE,gBAAgB,EAAE,CAAC,GACnE,CACR,CAAC,CAAC,CAAC,CACF,UAAU,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE;wBACrB,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,CAAC,CAAA;wBAC1E,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;wBAC9B,OAAO,CACL,kBAEE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,EAC7B,SAAS,EAAE,EAAE,CACX,0GAA0G,EAC1G,GAAG,IAAI,kCAAkC,CAC1C,aAED,eAAM,SAAS,EAAC,qCAAqC,YAAE,KAAK,GAAQ,EACpE,KAAC,IAAI,IAAC,IAAI,EAAC,OAAO,YAAE,GAAG,CAAC,EAAE,GAAQ,EACjC,GAAG,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,SAAS,EAAC,qBAAqB,GAAG,CAAC,CAAC,CAAC,IAAI,KAVlD,GAAG,CAAC,EAAE,CAWJ,CACV,CAAA;oBACH,CAAC,CAAC,CACH,GACG,EAEL,KAAK,CAAC,MAAM,GAAG,CAAC,IAAI,QAAQ,CAAC,CAAC,CAAC,CAC9B,KAAC,YAAY,cACX,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,QAAQ,aAI5B,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAClB,MAAC,MAAM,IACL,OAAO,EAAC,OAAO,EACf,SAAS,EAAC,yCAAyC,iBACvC,iBAAiB,EAC7B,OAAO,EAAE,GAAG,EAAE;oCACZ,QAAQ,CAAC,EAAE,CAAC,CAAA;oCACZ,YAAY,CAAC,KAAK,CAAC,CAAA;gCACrB,CAAC,aAED,KAAC,CAAC,KAAG,OAAE,SAAS,CAAC,eAAe,CAAC,IAC1B,CACV,CAAC,CAAC,CAAC,IAAI,EACR,KAAC,MAAM,KAAG,EACT,QAAQ,CAAC,CAAC,CAAC,CACV,8BACE,KAAC,KAAK,IAAC,OAAO,EAAC,SAAS,YAAE,SAAS,CAAC,4BAA4B,EAAE,EAAE,KAAK,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAS,EACtG,KAAC,MAAM,IAAC,OAAO,EAAC,OAAO,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,CAAC,YACvD,SAAS,CAAC,eAAe,CAAC,GACpB,EACT,KAAC,MAAM,IACL,OAAO,EAAE,GAAG,EAAE;4CACZ,QAAQ,CAAC,QAAQ,CAAC,CAAA;4CAClB,YAAY,CAAC,KAAK,CAAC,CAAA;wCACrB,CAAC,YAEA,SAAS,CAAC,cAAc,CAAC,GACnB,IACR,CACJ,CAAC,CAAC,CAAC,IAAI,IACJ,GACO,CAChB,CAAC,CAAC,CAAC,IAAI,IACM,GACT,CACV,CAAA;AACH,CAAC","sourcesContent":["import { Check, Search, X } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport type { DocumentRef } from '@elytracms/core/cms-core'\nimport {\n Badge,\n Button,\n Code,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/** Pick related document(s) from a target collection (brief §8.4, EC-074). */\nexport function RelationPicker({\n targetCollection,\n multiple,\n value,\n open,\n onOpenChange,\n onChange,\n excludeIds,\n}: {\n targetCollection: string\n multiple: boolean\n value: DocumentRef[]\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (refs: DocumentRef[]) => void\n /**\n * Document ids to omit from the candidate list (EC-218): a `parent` self-relation\n * passes the editing document plus its descendants, so a page can never be set\n * as its own ancestor (which would form a cycle).\n */\n excludeIds?: ReadonlySet<string>\n}) {\n const source = useCollectionView()\n const [query, setQuery] = useState('')\n const [selected, setSelected] = useState<DocumentRef[]>(value)\n\n // Seed selection only on open transitions — `value` is a fresh array each\n // parent render, so depending on it would reset in-progress multi-selection.\n useEffect(() => {\n if (open) {\n setSelected(value)\n setQuery('')\n }\n }, [open])\n\n const candidates = source.referenceCandidates(targetCollection).filter((d) => {\n if (excludeIds?.has(d.id)) return false\n if (!query.trim()) return true\n return source.titleOf({ collection: targetCollection, id: d.id }).toLowerCase().includes(query.toLowerCase())\n })\n\n const isSelected = (id: string) => selected.some((r) => r.id === id)\n\n function toggle(id: string) {\n const ref: DocumentRef = { collection: targetCollection, id }\n if (!multiple) {\n setSelected([ref])\n onChange([ref])\n onOpenChange(false)\n return\n }\n setSelected((prev) => (isSelected(id) ? prev.filter((r) => r.id !== id) : [...prev, ref]))\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>\n {translate(multiple ? 'cms.relation.pickTitle.many' : 'cms.relation.pickTitle.one')}\n </DialogTitle>\n <DialogDescription>\n From <Code tone=\"muted\">{targetCollection}</Code>\n {multiple ? ` · ${translate('cms.relation.selectMultipleHint')}` : ''}\n </DialogDescription>\n </DialogHeader>\n\n <div className=\"relative\">\n <Search className=\"pointer-events-none absolute left-2.5 top-1/2 size-3.5 -translate-y-1/2 text-muted-foreground\" />\n <Input\n autoFocus\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={translate('common.searchPlaceholder')}\n className=\"pl-8\"\n />\n </div>\n\n <div className=\"max-h-72 overflow-auto\">\n {candidates.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"p-3\">\n {translate('cms.relation.noDocuments', { collection: targetCollection })}\n </Text>\n ) : (\n candidates.map((doc) => {\n const title = source.titleOf({ collection: targetCollection, id: doc.id })\n const sel = isSelected(doc.id)\n return (\n <button\n key={doc.id}\n type=\"button\"\n onClick={() => toggle(doc.id)}\n className={cn(\n 'flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm transition-colors hover:bg-muted',\n sel && 'bg-accent text-accent-foreground',\n )}\n >\n <span className=\"min-w-0 flex-1 truncate font-medium\">{title}</span>\n <Code tone=\"muted\">{doc.id}</Code>\n {sel ? <Check className=\"size-4 text-primary\" /> : null}\n </button>\n )\n })\n )}\n </div>\n\n {value.length > 0 || multiple ? (\n <DialogFooter>\n <Row gap=\"2\" className=\"w-full\">\n {/* EC-246c: a way to CLEAR the relation from the picker — the only\n remove path for a cardinality-`one` relation edited inline in the\n list (the form field has its own per-row X). */}\n {value.length > 0 ? (\n <Button\n variant=\"ghost\"\n className=\"text-destructive hover:text-destructive\"\n data-testid=\"relation-remove\"\n onClick={() => {\n onChange([])\n onOpenChange(false)\n }}\n >\n <X /> {translate('common.remove')}\n </Button>\n ) : null}\n <Spacer />\n {multiple ? (\n <>\n <Badge variant=\"outline\">{translate('cms.relation.selectedCount', { count: selected.length })}</Badge>\n <Button variant=\"ghost\" onClick={() => onOpenChange(false)}>\n {translate('common.cancel')}\n </Button>\n <Button\n onClick={() => {\n onChange(selected)\n onOpenChange(false)\n }}\n >\n {translate('common.apply')}\n </Button>\n </>\n ) : null}\n </Row>\n </DialogFooter>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CMS image node (EC-314), extending TipTap's Image with a structured `ref`
|
|
3
|
+
* attribute (EC-301).
|
|
4
|
+
*
|
|
5
|
+
* An editorial image references a project asset by a self-typed envelope at
|
|
6
|
+
* `attrs.ref` (`{ type: 'asset', id }`) — the same envelope an asset field stores,
|
|
7
|
+
* so the reference-extractor indexes it as an asset use and the delivery renderer
|
|
8
|
+
* resolves it to the asset's URL (the literal `attrs.src` is only a preview/fallback
|
|
9
|
+
* for external images). `rendered: false` keeps `ref` out of the `<img>` markup
|
|
10
|
+
* while round-tripping through `getJSON()`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const AssetImage: import("@tiptap/core").Node<import("@tiptap/extension-image").ImageOptions, any>;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import Image from '@tiptap/extension-image';
|
|
2
|
+
/**
|
|
3
|
+
* The CMS image node (EC-314), extending TipTap's Image with a structured `ref`
|
|
4
|
+
* attribute (EC-301).
|
|
5
|
+
*
|
|
6
|
+
* An editorial image references a project asset by a self-typed envelope at
|
|
7
|
+
* `attrs.ref` (`{ type: 'asset', id }`) — the same envelope an asset field stores,
|
|
8
|
+
* so the reference-extractor indexes it as an asset use and the delivery renderer
|
|
9
|
+
* resolves it to the asset's URL (the literal `attrs.src` is only a preview/fallback
|
|
10
|
+
* for external images). `rendered: false` keeps `ref` out of the `<img>` markup
|
|
11
|
+
* while round-tripping through `getJSON()`.
|
|
12
|
+
*/
|
|
13
|
+
export const AssetImage = Image.extend({
|
|
14
|
+
addAttributes() {
|
|
15
|
+
return {
|
|
16
|
+
...this.parent?.(),
|
|
17
|
+
ref: {
|
|
18
|
+
default: null,
|
|
19
|
+
rendered: false,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
//# sourceMappingURL=asset-image.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-image.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/asset-image.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,yBAAyB,CAAA;AAE3C;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,CAAC,MAAM,CAAC;IACrC,aAAa;QACX,OAAO;YACL,GAAG,IAAI,CAAC,MAAM,EAAE,EAAE;YAClB,GAAG,EAAE;gBACH,OAAO,EAAE,IAAI;gBACb,QAAQ,EAAE,KAAK;aAChB;SACF,CAAA;IACH,CAAC;CACF,CAAC,CAAA","sourcesContent":["import Image from '@tiptap/extension-image'\n\n/**\n * The CMS image node (EC-314), extending TipTap's Image with a structured `ref`\n * attribute (EC-301).\n *\n * An editorial image references a project asset by a self-typed envelope at\n * `attrs.ref` (`{ type: 'asset', id }`) — the same envelope an asset field stores,\n * so the reference-extractor indexes it as an asset use and the delivery renderer\n * resolves it to the asset's URL (the literal `attrs.src` is only a preview/fallback\n * for external images). `rendered: false` keeps `ref` out of the `<img>` markup\n * while round-tripping through `getJSON()`.\n */\nexport const AssetImage = Image.extend({\n addAttributes() {\n return {\n ...this.parent?.(),\n ref: {\n default: null,\n rendered: false,\n },\n }\n },\n})\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Node } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* The rich-text component-embed node (EC-189/EC-314): a block atom holding a single
|
|
4
|
+
* composition value at `attrs.node`. It is the third place constrained composition
|
|
5
|
+
* lives (after the page collection and `blocks` fields) — a registry component
|
|
6
|
+
* embedded in prose. The node carries no ProseMirror content (the embedded component
|
|
7
|
+
* owns its own children through composition slots); `attrs.node` round-trips verbatim
|
|
8
|
+
* so the extractor still indexes the embedded component's references, and the delivery
|
|
9
|
+
* renderer renders it exactly as a `blocks` value.
|
|
10
|
+
*
|
|
11
|
+
* The editor shows a read-only placeholder card naming the embedded component (a live
|
|
12
|
+
* in-editor preview would need the runtime renderer + resolvers; deferred). Insert and
|
|
13
|
+
* deeper prop editing are wired from the toolbar / inspector.
|
|
14
|
+
*/
|
|
15
|
+
export declare const ComponentEmbed: Node<any, any>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react';
|
|
3
|
+
import { Blocks, Trash2 } from 'lucide-react';
|
|
4
|
+
import { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text';
|
|
5
|
+
import { Button } from '@elytracms/studio/ui';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
8
|
+
/**
|
|
9
|
+
* The rich-text component-embed node (EC-189/EC-314): a block atom holding a single
|
|
10
|
+
* composition value at `attrs.node`. It is the third place constrained composition
|
|
11
|
+
* lives (after the page collection and `blocks` fields) — a registry component
|
|
12
|
+
* embedded in prose. The node carries no ProseMirror content (the embedded component
|
|
13
|
+
* owns its own children through composition slots); `attrs.node` round-trips verbatim
|
|
14
|
+
* so the extractor still indexes the embedded component's references, and the delivery
|
|
15
|
+
* renderer renders it exactly as a `blocks` value.
|
|
16
|
+
*
|
|
17
|
+
* The editor shows a read-only placeholder card naming the embedded component (a live
|
|
18
|
+
* in-editor preview would need the runtime renderer + resolvers; deferred). Insert and
|
|
19
|
+
* deeper prop editing are wired from the toolbar / inspector.
|
|
20
|
+
*/
|
|
21
|
+
export const ComponentEmbed = Node.create({
|
|
22
|
+
name: COMPONENT_EMBED_NODE_TYPE,
|
|
23
|
+
group: 'block',
|
|
24
|
+
atom: true,
|
|
25
|
+
selectable: true,
|
|
26
|
+
draggable: true,
|
|
27
|
+
addAttributes() {
|
|
28
|
+
// `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.
|
|
29
|
+
return { node: { default: null, rendered: false } };
|
|
30
|
+
},
|
|
31
|
+
parseHTML() {
|
|
32
|
+
return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }];
|
|
33
|
+
},
|
|
34
|
+
renderHTML({ HTMLAttributes }) {
|
|
35
|
+
return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })];
|
|
36
|
+
},
|
|
37
|
+
addNodeView() {
|
|
38
|
+
return ReactNodeViewRenderer(ComponentEmbedView);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
/** Read the embedded ComponentNode from a ProseMirror node's attrs, if valid. */
|
|
42
|
+
function readEmbedded(attrNode) {
|
|
43
|
+
return embeddedComponentNode({
|
|
44
|
+
type: COMPONENT_EMBED_NODE_TYPE,
|
|
45
|
+
attrs: { node: attrNode },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function ComponentEmbedView({ node, deleteNode, selected, editor }) {
|
|
49
|
+
const embedded = readEmbedded(node.attrs.node);
|
|
50
|
+
const componentId = embedded?.componentId ?? translate('richText.embed.unknown');
|
|
51
|
+
const editable = editor.isEditable;
|
|
52
|
+
return (_jsxs(NodeViewWrapper, { "data-drag-handle": true, className: cn('my-2 flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm', selected && 'ring-2 ring-ring/50'), children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-foreground", children: componentId }), _jsx("span", { className: "shrink-0 text-xs text-muted-foreground", children: translate('richText.embed.badge') }), editable ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", contentEditable: false, "aria-label": translate('common.remove'), onClick: () => deleteNode(), children: _jsx(Trash2, { className: "size-3.5" }) })) : null] }));
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=component-embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"component-embed.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/component-embed.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AAE7F,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAC7C,OAAO,EAAE,yBAAyB,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AAE5F,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAEhD;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACxC,IAAI,EAAE,yBAAyB;IAC/B,KAAK,EAAE,OAAO;IACd,IAAI,EAAE,IAAI;IACV,UAAU,EAAE,IAAI;IAChB,SAAS,EAAE,IAAI;IAEf,aAAa;QACX,+EAA+E;QAC/E,OAAO,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,EAAE,CAAA;IACrD,CAAC;IAED,SAAS;QACP,OAAO,CAAC,EAAE,GAAG,EAAE,YAAY,yBAAyB,GAAG,EAAE,CAAC,CAAA;IAC5D,CAAC;IAED,UAAU,CAAC,EAAE,cAAc,EAAE;QAC3B,OAAO,CAAC,KAAK,EAAE,eAAe,CAAC,cAAc,EAAE,EAAE,CAAC,QAAQ,yBAAyB,EAAE,CAAC,EAAE,EAAE,EAAE,CAAC,CAAC,CAAA;IAChG,CAAC;IAED,WAAW;QACT,OAAO,qBAAqB,CAAC,kBAAkB,CAAC,CAAA;IAClD,CAAC;CACF,CAAC,CAAA;AAEF,iFAAiF;AACjF,SAAS,YAAY,CAAC,QAAiB;IACrC,OAAO,qBAAqB,CAAC;QAC3B,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,EAAE,IAAI,EAAE,QAAqB,EAAE;KACvC,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAsB;IACpF,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAC9C,MAAM,WAAW,GAAG,QAAQ,EAAE,WAAW,IAAI,SAAS,CAAC,wBAAwB,CAAC,CAAA;IAChF,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAA;IAElC,OAAO,CACL,MAAC,eAAe,8BAEd,SAAS,EAAE,EAAE,CACX,gGAAgG,EAChG,QAAQ,IAAI,qBAAqB,CAClC,aAED,KAAC,MAAM,IAAC,SAAS,EAAC,uCAAuC,GAAG,EAC5D,eAAM,SAAS,EAAC,qDAAqD,YAAE,WAAW,GAAQ,EAC1F,eAAM,SAAS,EAAC,wCAAwC,YAAE,SAAS,CAAC,sBAAsB,CAAC,GAAQ,EAClG,QAAQ,CAAC,CAAC,CAAC,CACV,KAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,SAAS,EACd,eAAe,EAAE,KAAK,gBACV,SAAS,CAAC,eAAe,CAAC,EACtC,OAAO,EAAE,GAAG,EAAE,CAAC,UAAU,EAAE,YAE3B,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACxB,CACV,CAAC,CAAC,CAAC,IAAI,IACQ,CACnB,CAAA;AACH,CAAC","sourcesContent":["import { Node, NodeViewWrapper, ReactNodeViewRenderer, mergeAttributes } from '@tiptap/react'\nimport type { ReactNodeViewProps } from '@tiptap/react'\nimport { Blocks, Trash2 } from 'lucide-react'\nimport { COMPONENT_EMBED_NODE_TYPE, embeddedComponentNode } from '@elytracms/core/rich-text'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { Button } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\n\n/**\n * The rich-text component-embed node (EC-189/EC-314): a block atom holding a single\n * composition value at `attrs.node`. It is the third place constrained composition\n * lives (after the page collection and `blocks` fields) — a registry component\n * embedded in prose. The node carries no ProseMirror content (the embedded component\n * owns its own children through composition slots); `attrs.node` round-trips verbatim\n * so the extractor still indexes the embedded component's references, and the delivery\n * renderer renders it exactly as a `blocks` value.\n *\n * The editor shows a read-only placeholder card naming the embedded component (a live\n * in-editor preview would need the runtime renderer + resolvers; deferred). Insert and\n * deeper prop editing are wired from the toolbar / inspector.\n */\nexport const ComponentEmbed = Node.create({\n name: COMPONENT_EMBED_NODE_TYPE,\n group: 'block',\n atom: true,\n selectable: true,\n draggable: true,\n\n addAttributes() {\n // `node` holds the embedded ComponentNode JSON; storage/JSON only, not markup.\n return { node: { default: null, rendered: false } }\n },\n\n parseHTML() {\n return [{ tag: `div[data-${COMPONENT_EMBED_NODE_TYPE}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['div', mergeAttributes(HTMLAttributes, { [`data-${COMPONENT_EMBED_NODE_TYPE}`]: '' })]\n },\n\n addNodeView() {\n return ReactNodeViewRenderer(ComponentEmbedView)\n },\n})\n\n/** Read the embedded ComponentNode from a ProseMirror node's attrs, if valid. */\nfunction readEmbedded(attrNode: unknown): ComponentNode | undefined {\n return embeddedComponentNode({\n type: COMPONENT_EMBED_NODE_TYPE,\n attrs: { node: attrNode as JsonValue },\n })\n}\n\nfunction ComponentEmbedView({ node, deleteNode, selected, editor }: ReactNodeViewProps) {\n const embedded = readEmbedded(node.attrs.node)\n const componentId = embedded?.componentId ?? translate('richText.embed.unknown')\n const editable = editor.isEditable\n\n return (\n <NodeViewWrapper\n data-drag-handle\n className={cn(\n 'my-2 flex items-center gap-2.5 rounded-md border border-border bg-muted/40 px-3 py-2.5 text-sm',\n selected && 'ring-2 ring-ring/50',\n )}\n >\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"min-w-0 flex-1 truncate font-medium text-foreground\">{componentId}</span>\n <span className=\"shrink-0 text-xs text-muted-foreground\">{translate('richText.embed.badge')}</span>\n {editable ? (\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-sm\"\n contentEditable={false}\n aria-label={translate('common.remove')}\n onClick={() => deleteNode()}\n >\n <Trash2 className=\"size-3.5\" />\n </Button>\n ) : null}\n </NodeViewWrapper>\n )\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
interface RichTextEditorProps {
|
|
2
|
+
/** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */
|
|
3
|
+
value: unknown;
|
|
4
|
+
onChange: (value: unknown) => void;
|
|
5
|
+
readOnly?: boolean;
|
|
6
|
+
id?: string;
|
|
7
|
+
/** The field's embed `allow` list (component ids editors may embed). */
|
|
8
|
+
allow?: string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared
|
|
12
|
+
* `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.
|
|
13
|
+
*
|
|
14
|
+
* It stays *controlled without churn*: the editor is created once from the initial
|
|
15
|
+
* value; the stored value is only re-pushed into ProseMirror when it changes
|
|
16
|
+
* externally (locale switch, reset, compare) and is not already equivalent to what
|
|
17
|
+
* is on screen. On edit, output is pruned to the lean storage convention
|
|
18
|
+
* ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back
|
|
19
|
+
* on its stored meaning, the *exact stored value* is re-emitted so the field never
|
|
20
|
+
* shows a spurious dirty state (the same "commit-nothing-when-equivalent" contract
|
|
21
|
+
* the canvas inline editor uses).
|
|
22
|
+
*/
|
|
23
|
+
export declare function RichTextEditor({ value, onChange, readOnly, id, allow }: RichTextEditorProps): import("react").JSX.Element | null;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useMemo, useRef } from 'react';
|
|
3
|
+
import { EditorContent, useEditor } from '@tiptap/react';
|
|
4
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
5
|
+
import { localizedLabel } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { useOptionalCollectionView } from '../../collection-view/collection-view-context';
|
|
7
|
+
import { useOptionalAssets } from '../../assets/assets-store';
|
|
8
|
+
import { buildRichTextExtensions } from './extensions';
|
|
9
|
+
import { isEffectivelyEmpty, isEquivalentToStored, normalizeEditorDoc, richTextValueFromDoc, toEditorDoc, } from './value';
|
|
10
|
+
import { RichTextToolbar } from './toolbar';
|
|
11
|
+
/**
|
|
12
|
+
* The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared
|
|
13
|
+
* `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.
|
|
14
|
+
*
|
|
15
|
+
* It stays *controlled without churn*: the editor is created once from the initial
|
|
16
|
+
* value; the stored value is only re-pushed into ProseMirror when it changes
|
|
17
|
+
* externally (locale switch, reset, compare) and is not already equivalent to what
|
|
18
|
+
* is on screen. On edit, output is pruned to the lean storage convention
|
|
19
|
+
* ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back
|
|
20
|
+
* on its stored meaning, the *exact stored value* is re-emitted so the field never
|
|
21
|
+
* shows a spurious dirty state (the same "commit-nothing-when-equivalent" contract
|
|
22
|
+
* the canvas inline editor uses).
|
|
23
|
+
*/
|
|
24
|
+
export function RichTextEditor({ value, onChange, readOnly = false, id, allow }) {
|
|
25
|
+
// Ambient context for reference-aware insertion (null outside a collection view /
|
|
26
|
+
// asset backend — the editor then degrades to external links and no image insert).
|
|
27
|
+
const source = useOptionalCollectionView();
|
|
28
|
+
const assets = useOptionalAssets();
|
|
29
|
+
// Every linkable document across the workspace (excluding the asset rail — you link
|
|
30
|
+
// to content, not to raw files). Recomputed only when the source changes.
|
|
31
|
+
const linkTargets = useMemo(() => {
|
|
32
|
+
if (!source)
|
|
33
|
+
return [];
|
|
34
|
+
const out = [];
|
|
35
|
+
for (const collection of source.collections) {
|
|
36
|
+
if (collection.id === source.assetCollectionId)
|
|
37
|
+
continue;
|
|
38
|
+
const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id);
|
|
39
|
+
for (const doc of source.referenceCandidates(collection.id)) {
|
|
40
|
+
const ref = { collection: collection.id, id: doc.id };
|
|
41
|
+
out.push({ ref, label: source.titleOf(ref), collectionLabel });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return out;
|
|
45
|
+
}, [source]);
|
|
46
|
+
// Latest value/onChange, read inside the stable onUpdate closure.
|
|
47
|
+
const valueRef = useRef(value);
|
|
48
|
+
valueRef.current = value;
|
|
49
|
+
const onChangeRef = useRef(onChange);
|
|
50
|
+
onChangeRef.current = onChange;
|
|
51
|
+
// What we last emitted upward — lets the sync effect ignore our own echoes.
|
|
52
|
+
const lastEmitted = useRef(value);
|
|
53
|
+
const editor = useEditor({
|
|
54
|
+
extensions: buildRichTextExtensions(),
|
|
55
|
+
content: toEditorDoc(value),
|
|
56
|
+
editable: !readOnly,
|
|
57
|
+
editorProps: {
|
|
58
|
+
attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },
|
|
59
|
+
},
|
|
60
|
+
onUpdate: ({ editor }) => {
|
|
61
|
+
const current = normalizeEditorDoc(editor.getJSON());
|
|
62
|
+
const stored = valueRef.current;
|
|
63
|
+
// Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);
|
|
64
|
+
// emptied → clear the field (undefined); otherwise → the new stamped value.
|
|
65
|
+
const next = isEquivalentToStored(current, stored)
|
|
66
|
+
? stored
|
|
67
|
+
: isEffectivelyEmpty(current)
|
|
68
|
+
? undefined
|
|
69
|
+
: richTextValueFromDoc(current);
|
|
70
|
+
lastEmitted.current = next;
|
|
71
|
+
onChangeRef.current(next);
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
// Reflect external value changes into the editor, never echoing our own emits.
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
if (!editor)
|
|
77
|
+
return;
|
|
78
|
+
if (value === lastEmitted.current)
|
|
79
|
+
return;
|
|
80
|
+
const current = normalizeEditorDoc(editor.getJSON());
|
|
81
|
+
if (isEquivalentToStored(current, value))
|
|
82
|
+
return;
|
|
83
|
+
editor.commands.setContent(toEditorDoc(value), { emitUpdate: false });
|
|
84
|
+
}, [editor, value]);
|
|
85
|
+
// Toggle editability without rebuilding the editor (preserves history/selection).
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
editor?.setEditable(!readOnly);
|
|
88
|
+
}, [editor, readOnly]);
|
|
89
|
+
if (!editor)
|
|
90
|
+
return null;
|
|
91
|
+
return (_jsxs("div", { className: cn('ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm', 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40', readOnly && 'opacity-70'), children: [!readOnly ? (_jsx(RichTextToolbar, { editor: editor, targets: linkTargets, assets: assets, allow: allow })) : null, _jsx(EditorContent, { editor: editor })] }));
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=editor.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"editor.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/editor.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEffect, useMemo, useRef } from 'react'\nimport { EditorContent, useEditor } from '@tiptap/react'\nimport type { RichTextDoc } from '@elytracms/core/rich-text'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\nimport { useOptionalCollectionView } from '../../collection-view/collection-view-context'\nimport { useOptionalAssets } from '../../assets/assets-store'\nimport { buildRichTextExtensions } from './extensions'\nimport {\n isEffectivelyEmpty,\n isEquivalentToStored,\n normalizeEditorDoc,\n richTextValueFromDoc,\n toEditorDoc,\n} from './value'\nimport { RichTextToolbar } from './toolbar'\nimport type { LinkTarget } from './link-control'\n\ninterface RichTextEditorProps {\n /** The stored `richText` value (`{ version, doc }`, legacy string, or empty). */\n value: unknown\n onChange: (value: unknown) => void\n readOnly?: boolean\n id?: string\n /** The field's embed `allow` list (component ids editors may embed). */\n allow?: string[]\n}\n\n/**\n * The CMS rich-text field editor (EC-314) — a real TipTap editor over the shared\n * `@elytracms/core/rich-text` storage, replacing the plaintext textarea stub.\n *\n * It stays *controlled without churn*: the editor is created once from the initial\n * value; the stored value is only re-pushed into ProseMirror when it changes\n * externally (locale switch, reset, compare) and is not already equivalent to what\n * is on screen. On edit, output is pruned to the lean storage convention\n * ({@link normalizeEditorDoc}) and — crucially — when an edit lands the document back\n * on its stored meaning, the *exact stored value* is re-emitted so the field never\n * shows a spurious dirty state (the same \"commit-nothing-when-equivalent\" contract\n * the canvas inline editor uses).\n */\nexport function RichTextEditor({ value, onChange, readOnly = false, id, allow }: RichTextEditorProps) {\n // Ambient context for reference-aware insertion (null outside a collection view /\n // asset backend — the editor then degrades to external links and no image insert).\n const source = useOptionalCollectionView()\n const assets = useOptionalAssets()\n\n // Every linkable document across the workspace (excluding the asset rail — you link\n // to content, not to raw files). Recomputed only when the source changes.\n const linkTargets = useMemo<LinkTarget[]>(() => {\n if (!source) return []\n const out: LinkTarget[] = []\n for (const collection of source.collections) {\n if (collection.id === source.assetCollectionId) continue\n const collectionLabel = localizedLabel(collection.form?.labelSingular, collection.id)\n for (const doc of source.referenceCandidates(collection.id)) {\n const ref = { collection: collection.id, id: doc.id }\n out.push({ ref, label: source.titleOf(ref), collectionLabel })\n }\n }\n return out\n }, [source])\n\n // Latest value/onChange, read inside the stable onUpdate closure.\n const valueRef = useRef(value)\n valueRef.current = value\n const onChangeRef = useRef(onChange)\n onChangeRef.current = onChange\n // What we last emitted upward — lets the sync effect ignore our own echoes.\n const lastEmitted = useRef<unknown>(value)\n\n const editor = useEditor({\n extensions: buildRichTextExtensions(),\n content: toEditorDoc(value),\n editable: !readOnly,\n editorProps: {\n attributes: { class: 'ec-rte-content', ...(id ? { id } : {}) },\n },\n onUpdate: ({ editor }) => {\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n const stored = valueRef.current\n // Three outcomes: unchanged → re-emit the stored value verbatim (no dirty);\n // emptied → clear the field (undefined); otherwise → the new stamped value.\n const next = isEquivalentToStored(current, stored)\n ? stored\n : isEffectivelyEmpty(current)\n ? undefined\n : richTextValueFromDoc(current)\n lastEmitted.current = next\n onChangeRef.current(next)\n },\n })\n\n // Reflect external value changes into the editor, never echoing our own emits.\n useEffect(() => {\n if (!editor) return\n if (value === lastEmitted.current) return\n const current = normalizeEditorDoc(editor.getJSON() as unknown as RichTextDoc)\n if (isEquivalentToStored(current, value)) return\n editor.commands.setContent(toEditorDoc(value), { emitUpdate: false })\n }, [editor, value])\n\n // Toggle editability without rebuilding the editor (preserves history/selection).\n useEffect(() => {\n editor?.setEditable(!readOnly)\n }, [editor, readOnly])\n\n if (!editor) return null\n\n return (\n <div\n className={cn(\n 'ec-rte overflow-hidden rounded-md border border-input bg-background shadow-sm',\n 'focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/40',\n readOnly && 'opacity-70',\n )}\n >\n {!readOnly ? (\n <RichTextToolbar editor={editor} targets={linkTargets} assets={assets} allow={allow} />\n ) : null}\n <EditorContent editor={editor} />\n </div>\n )\n}\n"]}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
interface EmbedControlProps {
|
|
3
|
+
editor: Editor;
|
|
4
|
+
/** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */
|
|
5
|
+
allow?: string[];
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Insert a registry component into prose as a `componentEmbed` (EC-189/EC-314): the
|
|
9
|
+
* third place constrained composition lives. The pick-list is the field's `allow`
|
|
10
|
+
* list (or every non-primitive component when open), matching the `blocks` field and
|
|
11
|
+
* the `validateRichTextEmbeds` leash. The inserted node holds a fresh `ComponentNode`
|
|
12
|
+
* built from manifest prop defaults; its props are edited from the embed's own
|
|
13
|
+
* inspector (deferred) — insertion + round-trip land here. Hidden when nothing is
|
|
14
|
+
* embeddable.
|
|
15
|
+
*/
|
|
16
|
+
export declare function EmbedControl({ editor, allow }: EmbedControlProps): import("react").JSX.Element | null;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo, useState } from 'react';
|
|
3
|
+
import { Blocks } from 'lucide-react';
|
|
4
|
+
import { COMPONENT_EMBED_NODE_TYPE } from '@elytracms/core/rich-text';
|
|
5
|
+
import { staticProp } from '@elytracms/core/project-graph';
|
|
6
|
+
import { isPrimitive } from '@elytracms/core/component-registry';
|
|
7
|
+
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
8
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input, Stack, Text, } from '@elytracms/studio/ui';
|
|
9
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
10
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
11
|
+
import { ToolbarButton } from './toolbar-button';
|
|
12
|
+
/**
|
|
13
|
+
* Insert a registry component into prose as a `componentEmbed` (EC-189/EC-314): the
|
|
14
|
+
* third place constrained composition lives. The pick-list is the field's `allow`
|
|
15
|
+
* list (or every non-primitive component when open), matching the `blocks` field and
|
|
16
|
+
* the `validateRichTextEmbeds` leash. The inserted node holds a fresh `ComponentNode`
|
|
17
|
+
* built from manifest prop defaults; its props are edited from the embed's own
|
|
18
|
+
* inspector (deferred) — insertion + round-trip land here. Hidden when nothing is
|
|
19
|
+
* embeddable.
|
|
20
|
+
*/
|
|
21
|
+
export function EmbedControl({ editor, allow }) {
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [query, setQuery] = useState('');
|
|
24
|
+
const embeddable = useMemo(() => {
|
|
25
|
+
const all = getStudioRegistry().registry.list();
|
|
26
|
+
const permitted = allow && allow.length > 0 ? all.filter((m) => allow.includes(m.id)) : all.filter((m) => !isPrimitive(m.id));
|
|
27
|
+
return permitted.filter((m) => m.clientRenderable !== false);
|
|
28
|
+
}, [allow]);
|
|
29
|
+
const results = useMemo(() => {
|
|
30
|
+
const q = query.trim().toLowerCase();
|
|
31
|
+
if (!q)
|
|
32
|
+
return embeddable;
|
|
33
|
+
return embeddable.filter((m) => (m.title ?? m.id).toLowerCase().includes(q) || m.id.toLowerCase().includes(q));
|
|
34
|
+
}, [embeddable, query]);
|
|
35
|
+
if (embeddable.length === 0)
|
|
36
|
+
return null;
|
|
37
|
+
function insert(manifest) {
|
|
38
|
+
editor
|
|
39
|
+
.chain()
|
|
40
|
+
.focus()
|
|
41
|
+
.insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embedNode(manifest) } })
|
|
42
|
+
.run();
|
|
43
|
+
setOpen(false);
|
|
44
|
+
setQuery('');
|
|
45
|
+
}
|
|
46
|
+
return (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: () => setOpen(true), children: _jsx(Blocks, { className: "size-3.5" }) }), _jsx(Dialog, { open: open, onOpenChange: setOpen, children: _jsxs(DialogContent, { className: "max-w-md", children: [_jsx(DialogHeader, { children: _jsx(DialogTitle, { children: translate('richText.embed.add') }) }), _jsxs(Stack, { gap: "2", children: [_jsx(Input, { autoFocus: true, value: query, placeholder: translate('richText.link.documentSearch'), onChange: (e) => setQuery(e.target.value) }), _jsx(Stack, { gap: "none", className: "max-h-72 overflow-y-auto", children: results.length === 0 ? (_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "px-1 py-2", children: translate('richText.embed.empty') })) : (results.map((manifest) => (_jsxs("button", { type: "button", onClick: () => insert(manifest), className: cn('flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left', 'hover:bg-muted focus-visible:bg-muted outline-none'), children: [_jsx(Blocks, { className: "size-4 shrink-0 text-muted-foreground" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col", children: [_jsx("span", { className: "truncate text-sm font-medium", children: manifest.title ?? manifest.id }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: manifest.id })] })] }, manifest.id)))) })] })] }) })] }));
|
|
47
|
+
}
|
|
48
|
+
/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */
|
|
49
|
+
function embedNode(manifest) {
|
|
50
|
+
const props = {};
|
|
51
|
+
for (const [name, spec] of Object.entries(manifest.props)) {
|
|
52
|
+
if (spec.default !== undefined)
|
|
53
|
+
props[name] = staticProp(spec.default);
|
|
54
|
+
}
|
|
55
|
+
return { id: newEmbedId(), componentId: manifest.id, props };
|
|
56
|
+
}
|
|
57
|
+
/** A unique id for an embedded node (studio runs in the browser; crypto is present). */
|
|
58
|
+
function newEmbedId() {
|
|
59
|
+
const uuid = globalThis.crypto?.randomUUID?.();
|
|
60
|
+
return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=embed-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-control.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,OAAO,EAAE,yBAAyB,EAAE,MAAM,2BAA2B,CAAA;AACrE,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAA;AAE1D,OAAO,EAAE,WAAW,EAAE,MAAM,oCAAoC,CAAA;AAEhE,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAA;AAClE,OAAO,EACL,MAAM,EACN,aAAa,EACb,YAAY,EACZ,WAAW,EACX,KAAK,EACL,KAAK,EACL,IAAI,GACL,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAQhD;;;;;;;;GAQG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAE,KAAK,EAAqB;IAC/D,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IACvC,MAAM,CAAC,KAAK,EAAE,QAAQ,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC,CAAA;IAEtC,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,EAAE;QAC9B,MAAM,GAAG,GAAG,iBAAiB,EAAE,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAA;QAC/C,MAAM,SAAS,GACb,KAAK,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7G,OAAO,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,gBAAgB,KAAK,KAAK,CAAC,CAAA;IAC9D,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,CAAA;IAEX,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAA;QACpC,IAAI,CAAC,CAAC;YAAE,OAAO,UAAU,CAAA;QACzB,OAAO,UAAU,CAAC,MAAM,CACtB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,CACrF,CAAA;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC,CAAA;IAEvB,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,IAAI,CAAA;IAExC,SAAS,MAAM,CAAC,QAA2B;QACzC,MAAM;aACH,KAAK,EAAE;aACP,KAAK,EAAE;aACP,aAAa,CAAC,EAAE,IAAI,EAAE,yBAAyB,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,SAAS,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;aACxF,GAAG,EAAE,CAAA;QACR,OAAO,CAAC,KAAK,CAAC,CAAA;QACd,QAAQ,CAAC,EAAE,CAAC,CAAA;IACd,CAAC;IAED,OAAO,CACL,8BACE,KAAC,aAAa,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,YACjF,KAAC,MAAM,IAAC,SAAS,EAAC,UAAU,GAAG,GACjB,EAChB,KAAC,MAAM,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,YACvC,MAAC,aAAa,IAAC,SAAS,EAAC,UAAU,aACjC,KAAC,YAAY,cACX,KAAC,WAAW,cAAE,SAAS,CAAC,oBAAoB,CAAC,GAAe,GAC/C,EACf,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,KAAK,IACJ,SAAS,QACT,KAAK,EAAE,KAAK,EACZ,WAAW,EAAE,SAAS,CAAC,8BAA8B,CAAC,EACtD,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,GACzC,EACF,KAAC,KAAK,IAAC,GAAG,EAAC,MAAM,EAAC,SAAS,EAAC,0BAA0B,YACnD,OAAO,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,CACtB,KAAC,IAAI,IAAC,EAAE,EAAC,MAAM,EAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAC,SAAS,EAAC,WAAW,YACzD,SAAS,CAAC,sBAAsB,CAAC,GAC7B,CACR,CAAC,CAAC,CAAC,CACF,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CACxB,kBAEE,IAAI,EAAC,QAAQ,EACb,OAAO,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAC/B,SAAS,EAAE,EAAE,CACX,iEAAiE,EACjE,oDAAoD,CACrD,aAED,KAAC,MAAM,IAAC,SAAS,EAAC,uCAAuC,GAAG,EAC5D,gBAAM,SAAS,EAAC,8BAA8B,aAC5C,eAAM,SAAS,EAAC,8BAA8B,YAAE,QAAQ,CAAC,KAAK,IAAI,QAAQ,CAAC,EAAE,GAAQ,EACrF,eAAM,SAAS,EAAC,wCAAwC,YAAE,QAAQ,CAAC,EAAE,GAAQ,IACxE,KAZF,QAAQ,CAAC,EAAE,CAaT,CACV,CAAC,CACH,GACK,IACF,IACM,GACT,IACR,CACJ,CAAA;AACH,CAAC;AAED,4FAA4F;AAC5F,SAAS,SAAS,CAAC,QAA2B;IAC5C,MAAM,KAAK,GAA2B,EAAE,CAAA;IACxC,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QAC1D,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS;YAAE,KAAK,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,IAAI,CAAC,OAAoB,CAAC,CAAA;IACrF,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,UAAU,EAAE,EAAE,WAAW,EAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,EAAE,CAAA;AAC9D,CAAC;AAED,wFAAwF;AACxF,SAAS,UAAU;IACjB,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAA;IAC9C,OAAO,IAAI,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAA;AAChF,CAAC","sourcesContent":["import { useMemo, useState } from 'react'\nimport { Blocks } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { COMPONENT_EMBED_NODE_TYPE } from '@elytracms/core/rich-text'\nimport { staticProp } from '@elytracms/core/project-graph'\nimport type { ComponentNode, JsonValue } from '@elytracms/core/project-graph'\nimport { isPrimitive } from '@elytracms/core/component-registry'\nimport type { ComponentManifest } from '@elytracms/core/component-registry'\nimport { getStudioRegistry } from '@elytracms/studio/lib/registry'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n Input,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { ToolbarButton } from './toolbar-button'\n\ninterface EmbedControlProps {\n editor: Editor\n /** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */\n allow?: string[]\n}\n\n/**\n * Insert a registry component into prose as a `componentEmbed` (EC-189/EC-314): the\n * third place constrained composition lives. The pick-list is the field's `allow`\n * list (or every non-primitive component when open), matching the `blocks` field and\n * the `validateRichTextEmbeds` leash. The inserted node holds a fresh `ComponentNode`\n * built from manifest prop defaults; its props are edited from the embed's own\n * inspector (deferred) — insertion + round-trip land here. Hidden when nothing is\n * embeddable.\n */\nexport function EmbedControl({ editor, allow }: EmbedControlProps) {\n const [open, setOpen] = useState(false)\n const [query, setQuery] = useState('')\n\n const embeddable = useMemo(() => {\n const all = getStudioRegistry().registry.list()\n const permitted =\n allow && allow.length > 0 ? all.filter((m) => allow.includes(m.id)) : all.filter((m) => !isPrimitive(m.id))\n return permitted.filter((m) => m.clientRenderable !== false)\n }, [allow])\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return embeddable\n return embeddable.filter(\n (m) => (m.title ?? m.id).toLowerCase().includes(q) || m.id.toLowerCase().includes(q),\n )\n }, [embeddable, query])\n\n if (embeddable.length === 0) return null\n\n function insert(manifest: ComponentManifest) {\n editor\n .chain()\n .focus()\n .insertContent({ type: COMPONENT_EMBED_NODE_TYPE, attrs: { node: embedNode(manifest) } })\n .run()\n setOpen(false)\n setQuery('')\n }\n\n return (\n <>\n <ToolbarButton label={translate('richText.embed.add')} onClick={() => setOpen(true)}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n <Dialog open={open} onOpenChange={setOpen}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('richText.embed.add')}</DialogTitle>\n </DialogHeader>\n <Stack gap=\"2\">\n <Input\n autoFocus\n value={query}\n placeholder={translate('richText.link.documentSearch')}\n onChange={(e) => setQuery(e.target.value)}\n />\n <Stack gap=\"none\" className=\"max-h-72 overflow-y-auto\">\n {results.length === 0 ? (\n <Text as=\"span\" size=\"xs\" tone=\"muted\" className=\"px-1 py-2\">\n {translate('richText.embed.empty')}\n </Text>\n ) : (\n results.map((manifest) => (\n <button\n key={manifest.id}\n type=\"button\"\n onClick={() => insert(manifest)}\n className={cn(\n 'flex w-full items-center gap-2.5 rounded-md px-2 py-2 text-left',\n 'hover:bg-muted focus-visible:bg-muted outline-none',\n )}\n >\n <Blocks className=\"size-4 shrink-0 text-muted-foreground\" />\n <span className=\"flex min-w-0 flex-1 flex-col\">\n <span className=\"truncate text-sm font-medium\">{manifest.title ?? manifest.id}</span>\n <span className=\"truncate text-xs text-muted-foreground\">{manifest.id}</span>\n </span>\n </button>\n ))\n )}\n </Stack>\n </Stack>\n </DialogContent>\n </Dialog>\n </>\n )\n}\n\n/** A fresh ComponentNode from a manifest's prop defaults (mirrors builder node-factory). */\nfunction embedNode(manifest: ComponentManifest): ComponentNode {\n const props: ComponentNode['props'] = {}\n for (const [name, spec] of Object.entries(manifest.props)) {\n if (spec.default !== undefined) props[name] = staticProp(spec.default as JsonValue)\n }\n return { id: newEmbedId(), componentId: manifest.id, props }\n}\n\n/** A unique id for an embedded node (studio runs in the browser; crypto is present). */\nfunction newEmbedId(): string {\n const uuid = globalThis.crypto?.randomUUID?.()\n return uuid ? `embed-${uuid}` : `embed-${Math.random().toString(36).slice(2)}`\n}\n"]}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Extensions } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* The TipTap extension set for the CMS rich-text editor (EC-314).
|
|
4
|
+
*
|
|
5
|
+
* Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)
|
|
6
|
+
* one-for-one so every editor document round-trips through `parseRichTextValue` /
|
|
7
|
+
* the delivery renderer with no format change:
|
|
8
|
+
*
|
|
9
|
+
* - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/
|
|
10
|
+
* listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks
|
|
11
|
+
* `bold/italic/code/strike/underline` — all ProseMirror-standard names identical
|
|
12
|
+
* to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so
|
|
13
|
+
* internal-document links can carry a structured `ref` (EC-290). Headings allow
|
|
14
|
+
* all six levels so existing `h4`–`h6` content is preserved even though the
|
|
15
|
+
* toolbar surfaces only 1–3.
|
|
16
|
+
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
17
|
+
* atoms (asset image, embedded registry component) so existing content of those
|
|
18
|
+
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
19
|
+
*
|
|
20
|
+
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
21
|
+
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildRichTextExtensions(): Extensions;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
2
|
+
import { Placeholder } from '@tiptap/extensions';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
import { RefLink } from './ref-link';
|
|
5
|
+
import { AssetImage } from './asset-image';
|
|
6
|
+
import { ComponentEmbed } from './component-embed';
|
|
7
|
+
/**
|
|
8
|
+
* The TipTap extension set for the CMS rich-text editor (EC-314).
|
|
9
|
+
*
|
|
10
|
+
* Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)
|
|
11
|
+
* one-for-one so every editor document round-trips through `parseRichTextValue` /
|
|
12
|
+
* the delivery renderer with no format change:
|
|
13
|
+
*
|
|
14
|
+
* - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/
|
|
15
|
+
* listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks
|
|
16
|
+
* `bold/italic/code/strike/underline` — all ProseMirror-standard names identical
|
|
17
|
+
* to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so
|
|
18
|
+
* internal-document links can carry a structured `ref` (EC-290). Headings allow
|
|
19
|
+
* all six levels so existing `h4`–`h6` content is preserved even though the
|
|
20
|
+
* toolbar surfaces only 1–3.
|
|
21
|
+
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
22
|
+
* atoms (asset image, embedded registry component) so existing content of those
|
|
23
|
+
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
24
|
+
*
|
|
25
|
+
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
26
|
+
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
27
|
+
*/
|
|
28
|
+
export function buildRichTextExtensions() {
|
|
29
|
+
return [
|
|
30
|
+
StarterKit.configure({
|
|
31
|
+
link: false,
|
|
32
|
+
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
33
|
+
}),
|
|
34
|
+
RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),
|
|
35
|
+
AssetImage,
|
|
36
|
+
ComponentEmbed,
|
|
37
|
+
Placeholder.configure({ placeholder: translate('richText.placeholder') }),
|
|
38
|
+
];
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=extensions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"extensions.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/extensions.ts"],"names":[],"mappings":"AAAA,OAAO,UAAU,MAAM,qBAAqB,CAAA;AAC5C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAA;AAEhD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAA;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO;QACL,UAAU,CAAC,SAAS,CAAC;YACnB,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE;SACxC,CAAC;QACF,OAAO,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;QAC5E,UAAU;QACV,cAAc;QACd,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAE,CAAC;KAC1E,CAAA;AACH,CAAC","sourcesContent":["import StarterKit from '@tiptap/starter-kit'\nimport { Placeholder } from '@tiptap/extensions'\nimport type { Extensions } from '@tiptap/react'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { RefLink } from './ref-link'\nimport { AssetImage } from './asset-image'\nimport { ComponentEmbed } from './component-embed'\n\n/**\n * The TipTap extension set for the CMS rich-text editor (EC-314).\n *\n * Deliberately matches the storage vocabulary (`@elytracms/core/rich-text` schema)\n * one-for-one so every editor document round-trips through `parseRichTextValue` /\n * the delivery renderer with no format change:\n *\n * - **StarterKit** supplies `doc/paragraph/text/heading/bulletList/orderedList/\n * listItem/blockquote/codeBlock/hardBreak/horizontalRule` and the marks\n * `bold/italic/code/strike/underline` — all ProseMirror-standard names identical\n * to storage. Its bundled `link` is disabled and replaced by {@link RefLink} so\n * internal-document links can carry a structured `ref` (EC-290). Headings allow\n * all six levels so existing `h4`–`h6` content is preserved even though the\n * toolbar surfaces only 1–3.\n * - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying\n * atoms (asset image, embedded registry component) so existing content of those\n * kinds survives a round-trip verbatim rather than being dropped by the schema.\n *\n * No `allow` gating lives here — that governs *insertion* (the embed picker) and\n * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.\n */\nexport function buildRichTextExtensions(): Extensions {\n return [\n StarterKit.configure({\n link: false,\n heading: { levels: [1, 2, 3, 4, 5, 6] },\n }),\n RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),\n AssetImage,\n ComponentEmbed,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n ]\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import type { AssetsStore } from '../../assets/assets-store';
|
|
3
|
+
interface ImageControlProps {
|
|
4
|
+
editor: Editor;
|
|
5
|
+
assets: AssetsStore | null;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Insert an editorial image (EC-314/EC-301): pick an asset from the project library
|
|
9
|
+
* and drop an `image` node carrying the canonical asset envelope at `attrs.ref`
|
|
10
|
+
* (`{ type: 'asset', id }`) — so the reference-extractor indexes it as an asset use
|
|
11
|
+
* and delivery resolves it to the live URL. `attrs.src` snapshots the current URL for
|
|
12
|
+
* the editor preview and as delivery's external fallback. Rendered only when an asset
|
|
13
|
+
* backend is present.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ImageControl({ editor, assets }: ImageControlProps): import("react").JSX.Element | null;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { ImagePlus } from 'lucide-react';
|
|
4
|
+
import { assetNode } from '@elytracms/core/cms-core';
|
|
5
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
|
+
import { AssetPicker } from '../asset-picker';
|
|
7
|
+
import { ToolbarButton } from './toolbar-button';
|
|
8
|
+
/**
|
|
9
|
+
* Insert an editorial image (EC-314/EC-301): pick an asset from the project library
|
|
10
|
+
* and drop an `image` node carrying the canonical asset envelope at `attrs.ref`
|
|
11
|
+
* (`{ type: 'asset', id }`) — so the reference-extractor indexes it as an asset use
|
|
12
|
+
* and delivery resolves it to the live URL. `attrs.src` snapshots the current URL for
|
|
13
|
+
* the editor preview and as delivery's external fallback. Rendered only when an asset
|
|
14
|
+
* backend is present.
|
|
15
|
+
*/
|
|
16
|
+
export function ImageControl({ editor, assets }) {
|
|
17
|
+
const [open, setOpen] = useState(false);
|
|
18
|
+
if (!assets)
|
|
19
|
+
return null;
|
|
20
|
+
function insert(assetId) {
|
|
21
|
+
setOpen(false);
|
|
22
|
+
if (!assetId)
|
|
23
|
+
return;
|
|
24
|
+
const record = assets.assets.find((a) => a.id === assetId);
|
|
25
|
+
editor
|
|
26
|
+
.chain()
|
|
27
|
+
.focus()
|
|
28
|
+
.insertContent({
|
|
29
|
+
type: 'image',
|
|
30
|
+
attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },
|
|
31
|
+
})
|
|
32
|
+
.run();
|
|
33
|
+
}
|
|
34
|
+
return (_jsxs(_Fragment, { children: [_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: () => setOpen(true), children: _jsx(ImagePlus, { className: "size-3.5" }) }), _jsx(AssetPicker, { open: open, onOpenChange: setOpen, value: null, onChange: insert, accept: "image/*" })] }));
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=image-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/image-control.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAExC,OAAO,EAAE,SAAS,EAAE,MAAM,0BAA0B,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAA;AAC7C,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAOhD;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,EAAE,MAAM,EAAE,MAAM,EAAqB;IAChE,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAEvC,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAA;IAExB,SAAS,MAAM,CAAC,OAAsB;QACpC,OAAO,CAAC,KAAK,CAAC,CAAA;QACd,IAAI,CAAC,OAAO;YAAE,OAAM;QACpB,MAAM,MAAM,GAAG,MAAO,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,CAAA;QAC3D,MAAM;aACH,KAAK,EAAE;aACP,KAAK,EAAE;aACP,aAAa,CAAC;YACb,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,CAAC,OAAO,CAAC,EAAE;SACnF,CAAC;aACD,GAAG,EAAE,CAAA;IACV,CAAC;IAED,OAAO,CACL,8BACE,KAAC,aAAa,IAAC,KAAK,EAAE,SAAS,CAAC,oBAAoB,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,YACjF,KAAC,SAAS,IAAC,SAAS,EAAC,UAAU,GAAG,GACpB,EAChB,KAAC,WAAW,IAAC,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAC,SAAS,GAAG,IACjG,CACJ,CAAA;AACH,CAAC","sourcesContent":["import { useState } from 'react'\nimport { ImagePlus } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { assetNode } from '@elytracms/core/cms-core'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { AssetsStore } from '../../assets/assets-store'\nimport { AssetPicker } from '../asset-picker'\nimport { ToolbarButton } from './toolbar-button'\n\ninterface ImageControlProps {\n editor: Editor\n assets: AssetsStore | null\n}\n\n/**\n * Insert an editorial image (EC-314/EC-301): pick an asset from the project library\n * and drop an `image` node carrying the canonical asset envelope at `attrs.ref`\n * (`{ type: 'asset', id }`) — so the reference-extractor indexes it as an asset use\n * and delivery resolves it to the live URL. `attrs.src` snapshots the current URL for\n * the editor preview and as delivery's external fallback. Rendered only when an asset\n * backend is present.\n */\nexport function ImageControl({ editor, assets }: ImageControlProps) {\n const [open, setOpen] = useState(false)\n\n if (!assets) return null\n\n function insert(assetId: string | null) {\n setOpen(false)\n if (!assetId) return\n const record = assets!.assets.find((a) => a.id === assetId)\n editor\n .chain()\n .focus()\n .insertContent({\n type: 'image',\n attrs: { src: record?.url ?? '', alt: record?.alt ?? '', ref: assetNode(assetId) },\n })\n .run()\n }\n\n return (\n <>\n <ToolbarButton label={translate('richText.image.add')} onClick={() => setOpen(true)}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n <AssetPicker open={open} onOpenChange={setOpen} value={null} onChange={insert} accept=\"image/*\" />\n </>\n )\n}\n"]}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CMS rich-text field editor (EC-314) — a TipTap editor over the shared
|
|
3
|
+
* `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}
|
|
4
|
+
* component wired into the `richText` field control; everything else (extensions,
|
|
5
|
+
* toolbar, value bridging) is an implementation detail.
|
|
6
|
+
*/
|
|
7
|
+
export { RichTextEditor } from './editor';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CMS rich-text field editor (EC-314) — a TipTap editor over the shared
|
|
3
|
+
* `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}
|
|
4
|
+
* component wired into the `richText` field control; everything else (extensions,
|
|
5
|
+
* toolbar, value bridging) is an implementation detail.
|
|
6
|
+
*/
|
|
7
|
+
export { RichTextEditor } from './editor';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,UAAU,CAAA","sourcesContent":["/**\n * The CMS rich-text field editor (EC-314) — a TipTap editor over the shared\n * `@elytracms/core/rich-text` storage. Public surface is the {@link RichTextEditor}\n * component wired into the `richText` field control; everything else (extensions,\n * toolbar, value bridging) is an implementation detail.\n */\nexport { RichTextEditor } from './editor'\n"]}
|