@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
|
@@ -118,13 +118,19 @@ function AssetPickerDialog({ value, onOpenChange, onChange, accept, }) {
|
|
|
118
118
|
function AssetPickerCard({ asset, selected, onPick, }) {
|
|
119
119
|
return (_jsxs("button", { type: "button", onClick: onPick, className: cn('flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors', selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted'), "data-testid": "asset-picker-card", children: [_jsx(PickerThumbnail, { asset: asset }), _jsxs(Row, { gap: "1", children: [_jsx(Text, { size: "xs", className: "min-w-0 flex-1 truncate font-medium", children: asset.title ?? asset.filename }), selected ? _jsx(Check, { className: "size-3.5 text-primary" }) : null] }), _jsx(Text, { size: "xs", tone: asset.alt ? 'muted' : 'subtle', className: "truncate", children: asset.alt ?? translate('cms.asset.noAltText') })] }));
|
|
120
120
|
}
|
|
121
|
-
/** Image preview
|
|
121
|
+
/** Image preview: the EC-400 downscaled thumb when resolvable, else the full
|
|
122
|
+
* original; icon fallback for other types. In-memory/opaque locators
|
|
123
|
+
* (memory://, convex://) are not fetchable by <img>, so those degrade too. */
|
|
122
124
|
function PickerThumbnail({ asset }) {
|
|
123
125
|
const isImage = asset.contentType.startsWith('image/');
|
|
124
|
-
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
const thumbUrl = asset.thumb?.url;
|
|
127
|
+
const src = thumbUrl?.startsWith('http') === true
|
|
128
|
+
? thumbUrl
|
|
129
|
+
: asset.url?.startsWith('http') === true
|
|
130
|
+
? asset.url
|
|
131
|
+
: undefined;
|
|
132
|
+
if (isImage && src) {
|
|
133
|
+
return (_jsx("img", { src: src, alt: asset.alt ?? asset.filename, className: "aspect-video w-full rounded-md bg-muted object-cover" }));
|
|
128
134
|
}
|
|
129
135
|
return (_jsx("span", { className: "grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground", children: _jsx(ImageIcon, { className: "size-6" }) }));
|
|
130
136
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Maximize2, Minimize2, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { filterAssets } from '../assets/asset-search'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field. The dialog\n * is form-width with a maximize toggle and supports drag/click upload that\n * auto-selects the new asset (EC-301).\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open dialog over a mounted assets store. */\nfunction AssetPickerDialog({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [maximized, setMaximized] = useState(false)\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = filterAssets(store.assets, query)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n onChange(added[0]!)\n onOpenChange(false)\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // Radix portals the dialog in the DOM, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the dialog would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(maximized ? 'max-w-[95vw]' : 'max-w-2xl')}\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <DialogHeader>\n <Row gap=\"2\" align=\"center\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </Stack>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={maximized ? translate('cms.asset.restoreDialogSize') : translate('cms.asset.maximizeDialog')}\n onClick={() => setMaximized((v) => !v)}\n className=\"mr-7\"\n >\n {maximized ? <Minimize2 className=\"size-4\" /> : <Maximize2 className=\"size-4\" />}\n </Button>\n </Row>\n </DialogHeader>\n\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\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('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className={cn(\n 'grid gap-2 overflow-auto',\n maximized\n ? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'\n : 'max-h-[60vh] grid-cols-3 sm:grid-cols-4',\n )}\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim()\n ? translate('cms.asset.noMatches')\n : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => {\n onChange(asset.id)\n onOpenChange(false)\n }}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n onChange(null)\n onOpenChange(false)\n }}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** One selectable asset: thumbnail + title/filename + alt text. The internal\n * asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n >\n <PickerThumbnail asset={asset} />\n <Row gap=\"1\">\n <Text size=\"xs\" className=\"min-w-0 flex-1 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {selected ? <Check className=\"size-3.5 text-primary\" /> : null}\n </Row>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n\n/** Image preview from the stored serve URL; icon fallback for other types. */\nfunction PickerThumbnail({ asset }: { asset: AssetRecord }) {\n const isImage = asset.contentType.startsWith('image/')\n // In-memory/opaque locators (memory://, convex://) are not fetchable by <img>.\n const resolvable = asset.url?.startsWith('http') ?? false\n if (isImage && asset.url && resolvable) {\n return (\n <img\n src={asset.url}\n alt={asset.alt ?? asset.filename}\n className=\"aspect-video w-full rounded-md bg-muted object-cover\"\n />\n )\n }\n return (\n <span className=\"grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground\">\n <ImageIcon className=\"size-6\" />\n </span>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"asset-picker.js","sourceRoot":"","sources":["../../../src/components/cms/asset-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { AlertTriangle, Check, ImageIcon, Maximize2, Minimize2, Search, Upload } from 'lucide-react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport {\n Button,\n Dialog,\n DialogContent,\n DialogDescription,\n DialogHeader,\n DialogTitle,\n Input,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { filterAssets } from '../assets/asset-search'\nimport { AssetsProvider, useAssets, useOptionalAssets } from '../assets/assets-store'\nimport { newlyAddedAssetIds } from '../assets/focal-point'\nimport { useCollectionView } from '../collection-view/collection-view-context'\n\n/**\n * Pick an asset for a CMS `asset` field (brief §8.4, §8.6, EC-075 → EC-152).\n * Backed by the *real* asset library (the assets store over the operations\n * client) — the same records the Assets section lists — not by fixture\n * documents. Selecting stores the asset id on the document field. The dialog\n * is form-width with a maximize toggle and supports drag/click upload that\n * auto-selects the new asset (EC-301).\n */\nexport function AssetPicker({\n value,\n open,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n open: boolean\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n /** The file input's `accept` attribute for in-picker upload (field-derived). */\n accept?: string\n}) {\n const source = useCollectionView()\n const ambient = useOptionalAssets()\n // Mount nothing while closed: the self-provided assets store would\n // otherwise load the library eagerly for every asset field on the form.\n if (!open) return null\n if (ambient) {\n return (\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n )\n }\n if (source.projectId === undefined) {\n // No project scope (fixture-only CMS session): the library is\n // unreachable — say so instead of silently showing nothing.\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent className=\"max-w-md\">\n <DialogHeader>\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </DialogHeader>\n <Row gap=\"2\" data-testid=\"asset-picker-no-project\">\n <AlertTriangle className=\"size-4 text-warning\" />\n <Text size=\"sm\" tone=\"muted\">\n {translate('cms.asset.noProject')}\n </Text>\n </Row>\n </DialogContent>\n </Dialog>\n )\n }\n return (\n <AssetsProvider projectId={source.projectId}>\n <AssetPickerDialog value={value} onOpenChange={onOpenChange} onChange={onChange} accept={accept} />\n </AssetsProvider>\n )\n}\n\n/** The open dialog over a mounted assets store. */\nfunction AssetPickerDialog({\n value,\n onOpenChange,\n onChange,\n accept,\n}: {\n value: string | null\n onOpenChange: (open: boolean) => void\n onChange: (assetId: string | null) => void\n accept?: string\n}) {\n const store = useAssets()\n // Fresh per open: the picker unmounts entirely while closed.\n const [query, setQuery] = useState('')\n const [maximized, setMaximized] = useState(false)\n const [dragging, setDragging] = useState(false)\n const fileInputRef = useRef<HTMLInputElement>(null)\n // Library ids when an upload started; the effect below diffs against it to\n // learn the new asset and select it (the `upload()` API returns nothing).\n const pendingRef = useRef<ReadonlySet<string> | null>(null)\n // Drag depth (EC-301): keeps the drop overlay steady as the pointer crosses\n // child boundaries — the same per-element dragenter/dragleave flicker the field\n // fixes. Only hide when the count returns to zero.\n const dragDepth = useRef(0)\n const assets = filterAssets(store.assets, query)\n const libraryIds = useMemo(() => store.assets.map((a) => a.id), [store.assets])\n\n // Auto-select an asset uploaded from within the picker, then close.\n useEffect(() => {\n if (!pendingRef.current) return\n const added = newlyAddedAssetIds(pendingRef.current, libraryIds)\n if (added.length === 0) return\n pendingRef.current = null\n onChange(added[0]!)\n onOpenChange(false)\n }, [libraryIds, onChange, onOpenChange])\n\n function handleUpload(files: File[]) {\n if (files.length === 0) return\n pendingRef.current = new Set(libraryIds)\n store.upload(files)\n }\n // Radix portals the dialog in the DOM, but React synthetic events still bubble up\n // the COMPONENT tree — and this picker is rendered as a child of the asset field,\n // which has its own drag-to-upload handlers. Without stopPropagation a drop inside\n // the dialog would fire BOTH the picker's and the field's `onDrop`, uploading the\n // file twice. Stop every DnD event here so the picker owns its drop entirely.\n function onDragEnter(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current += 1\n if (!dragging) setDragging(true)\n }\n function onDragOver(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n }\n function onDragLeave(e: React.DragEvent) {\n e.stopPropagation()\n dragDepth.current -= 1\n if (dragDepth.current <= 0) {\n dragDepth.current = 0\n setDragging(false)\n }\n }\n function onDrop(e: React.DragEvent) {\n e.preventDefault()\n e.stopPropagation()\n dragDepth.current = 0\n setDragging(false)\n handleUpload(Array.from(e.dataTransfer.files))\n }\n\n return (\n <Dialog open onOpenChange={onOpenChange}>\n <DialogContent\n className={cn(maximized ? 'max-w-[95vw]' : 'max-w-2xl')}\n onDragEnter={onDragEnter}\n onDragOver={onDragOver}\n onDragLeave={onDragLeave}\n onDrop={onDrop}\n >\n <DialogHeader>\n <Row gap=\"2\" align=\"center\">\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <DialogTitle>{translate('cms.asset.pickTitle')}</DialogTitle>\n <DialogDescription>{translate('cms.asset.pickDescription')}</DialogDescription>\n </Stack>\n <Button\n variant=\"ghost\"\n size=\"icon\"\n aria-label={maximized ? translate('cms.asset.restoreDialogSize') : translate('cms.asset.maximizeDialog')}\n onClick={() => setMaximized((v) => !v)}\n className=\"mr-7\"\n >\n {maximized ? <Minimize2 className=\"size-4\" /> : <Maximize2 className=\"size-4\" />}\n </Button>\n </Row>\n </DialogHeader>\n\n <Row gap=\"2\" align=\"center\">\n <span className=\"relative flex-1\">\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('cms.asset.searchPlaceholder')}\n className=\"pl-8\"\n data-testid=\"asset-picker-search\"\n />\n </span>\n <Button variant=\"outline\" size=\"sm\" onClick={() => fileInputRef.current?.click()}>\n <Upload /> {translate('common.upload')}\n </Button>\n <input\n ref={fileInputRef}\n type=\"file\"\n multiple\n accept={accept ?? 'image/*'}\n className=\"hidden\"\n onChange={(e) => {\n handleUpload(Array.from(e.target.files ?? []))\n e.target.value = ''\n }}\n />\n </Row>\n\n <div\n className={cn(\n 'grid gap-2 overflow-auto',\n maximized\n ? 'max-h-[78vh] grid-cols-4 lg:grid-cols-6'\n : 'max-h-[60vh] grid-cols-3 sm:grid-cols-4',\n )}\n data-testid=\"asset-picker-grid\"\n >\n {store.status === 'loading' ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {translate('cms.asset.loading')}\n </Text>\n ) : store.status === 'error' ? (\n <Row gap=\"2\" className=\"col-span-full p-3\" data-testid=\"asset-picker-error\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? translate('cms.asset.loadError')}\n </Text>\n </Row>\n ) : assets.length === 0 ? (\n <Text size=\"sm\" tone=\"muted\" className=\"col-span-full p-3\">\n {query.trim()\n ? translate('cms.asset.noMatches')\n : translate('cms.asset.empty')}\n </Text>\n ) : (\n assets.map((asset) => (\n <AssetPickerCard\n key={asset.id}\n asset={asset}\n selected={asset.id === value}\n onPick={() => {\n onChange(asset.id)\n onOpenChange(false)\n }}\n />\n ))\n )}\n </div>\n\n <Row gap=\"2\">\n <Spacer />\n {value ? (\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n onChange(null)\n onOpenChange(false)\n }}\n >\n {translate('common.clear')}\n </Button>\n ) : null}\n </Row>\n\n {dragging ? (\n <div className=\"pointer-events-none absolute inset-0 z-10 grid place-items-center rounded-xl border-2 border-dashed border-primary bg-background/85\">\n <Row gap=\"2\" align=\"center\">\n <Upload className=\"size-5 text-primary\" />\n <Text className=\"font-medium\">{translate('cms.asset.dropToUpload')}</Text>\n </Row>\n </div>\n ) : null}\n </DialogContent>\n </Dialog>\n )\n}\n\n/** One selectable asset: thumbnail + title/filename + alt text. The internal\n * asset id is intentionally not shown — it's noise to editors (EC-301). */\nfunction AssetPickerCard({\n asset,\n selected,\n onPick,\n}: {\n asset: AssetRecord\n selected: boolean\n onPick: () => void\n}) {\n return (\n <button\n type=\"button\"\n onClick={onPick}\n className={cn(\n 'flex flex-col gap-1.5 rounded-lg border p-2 text-left transition-colors',\n selected ? 'border-primary bg-primary/5' : 'border-border hover:bg-muted',\n )}\n data-testid=\"asset-picker-card\"\n >\n <PickerThumbnail asset={asset} />\n <Row gap=\"1\">\n <Text size=\"xs\" className=\"min-w-0 flex-1 truncate font-medium\">\n {asset.title ?? asset.filename}\n </Text>\n {selected ? <Check className=\"size-3.5 text-primary\" /> : null}\n </Row>\n <Text size=\"xs\" tone={asset.alt ? 'muted' : 'subtle'} className=\"truncate\">\n {asset.alt ?? translate('cms.asset.noAltText')}\n </Text>\n </button>\n )\n}\n\n/** Image preview: the EC-400 downscaled thumb when resolvable, else the full\n * original; icon fallback for other types. In-memory/opaque locators\n * (memory://, convex://) are not fetchable by <img>, so those degrade too. */\nfunction PickerThumbnail({ asset }: { asset: AssetRecord }) {\n const isImage = asset.contentType.startsWith('image/')\n const thumbUrl = asset.thumb?.url\n const src =\n thumbUrl?.startsWith('http') === true\n ? thumbUrl\n : asset.url?.startsWith('http') === true\n ? asset.url\n : undefined\n if (isImage && src) {\n return (\n <img\n src={src}\n alt={asset.alt ?? asset.filename}\n className=\"aspect-video w-full rounded-md bg-muted object-cover\"\n />\n )\n }\n return (\n <span className=\"grid aspect-video w-full place-items-center rounded-md bg-muted text-muted-foreground\">\n <ImageIcon className=\"size-6\" />\n </span>\n )\n}\n"]}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* Shared block-style vocabulary for the rich-text toolbar (EC-418).
|
|
4
|
+
*
|
|
5
|
+
* The block-style dropdown's TRIGGER and its MENU ITEMS must show the SAME
|
|
6
|
+
* icon-per-style so they can never drift: paragraph → the lucide `Pilcrow`, a
|
|
7
|
+
* heading → its compact `H{level}` badge. Declaring the mapping ONCE here —
|
|
8
|
+
* consumed by both — makes that drift structurally impossible, the same pattern
|
|
9
|
+
* `block-commands.ts` (EC-328) uses for the list/quote/code commands.
|
|
10
|
+
*
|
|
11
|
+
* This also underpins the EC-418 width-stability fix: the trigger renders the
|
|
12
|
+
* ACTIVE style's fixed-width glyph instead of its variable-width localized label
|
|
13
|
+
* ("Absatz" ≪ "Überschrift 2"), so caret movement across styles never reflows the
|
|
14
|
+
* strip. The full localized labels appear only in the opened menu, and the trigger
|
|
15
|
+
* keeps its accessible name via {@link blockStyleLabel} (an `aria-label`).
|
|
16
|
+
*/
|
|
17
|
+
/** A block-style option: paragraph (`headingLevel: null`) or a heading level. */
|
|
18
|
+
export interface BlockStyleDescriptor {
|
|
19
|
+
/** Heading level, or `null` for paragraph (the base body style). */
|
|
20
|
+
headingLevel: number | null;
|
|
21
|
+
/** Apply this style on an already-`.focus()`ed chain. */
|
|
22
|
+
chain: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>;
|
|
23
|
+
}
|
|
24
|
+
/** The block styles the toolbar offers, in menu order (paragraph, then H1–H3). */
|
|
25
|
+
export declare const BLOCK_STYLES: readonly BlockStyleDescriptor[];
|
|
26
|
+
/**
|
|
27
|
+
* The pure icon-per-style decision (EC-418) — the single source both the trigger
|
|
28
|
+
* and the menu items derive their glyph from (via {@link BlockStyleGlyph}). Kept
|
|
29
|
+
* data-only (no JSX) so it is unit-testable in a node env without a DOM.
|
|
30
|
+
*/
|
|
31
|
+
export type BlockStyleIcon = {
|
|
32
|
+
kind: 'paragraph';
|
|
33
|
+
} | {
|
|
34
|
+
kind: 'heading';
|
|
35
|
+
level: number;
|
|
36
|
+
};
|
|
37
|
+
export declare function blockStyleIcon(headingLevel: number | null): BlockStyleIcon;
|
|
38
|
+
/** The localized label for a block style — the menu-item text and the trigger's a11y name. */
|
|
39
|
+
export declare function blockStyleLabel(headingLevel: number | null): string;
|
|
40
|
+
/**
|
|
41
|
+
* The one glyph rendered for a block style, shared by the trigger and the menu
|
|
42
|
+
* items (EC-418). Fixed-width by construction: the `Pilcrow` icon is `size-3.5`
|
|
43
|
+
* (14px) and the heading badge is `w-3.5` (14px), so the active-style glyph never
|
|
44
|
+
* changes the trigger's width as the selection moves across styles.
|
|
45
|
+
*/
|
|
46
|
+
export declare function BlockStyleGlyph({ headingLevel }: {
|
|
47
|
+
headingLevel: number | null;
|
|
48
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pilcrow } from 'lucide-react';
|
|
3
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
/** The block styles the toolbar offers, in menu order (paragraph, then H1–H3). */
|
|
5
|
+
export const BLOCK_STYLES = [
|
|
6
|
+
{ headingLevel: null, chain: (c) => c.setParagraph() },
|
|
7
|
+
{ headingLevel: 1, chain: (c) => c.toggleHeading({ level: 1 }) },
|
|
8
|
+
{ headingLevel: 2, chain: (c) => c.toggleHeading({ level: 2 }) },
|
|
9
|
+
{ headingLevel: 3, chain: (c) => c.toggleHeading({ level: 3 }) },
|
|
10
|
+
];
|
|
11
|
+
export function blockStyleIcon(headingLevel) {
|
|
12
|
+
return headingLevel == null ? { kind: 'paragraph' } : { kind: 'heading', level: headingLevel };
|
|
13
|
+
}
|
|
14
|
+
/** The localized label for a block style — the menu-item text and the trigger's a11y name. */
|
|
15
|
+
export function blockStyleLabel(headingLevel) {
|
|
16
|
+
return headingLevel == null
|
|
17
|
+
? translate('richText.paragraph')
|
|
18
|
+
: translate('richText.heading', { level: headingLevel });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The one glyph rendered for a block style, shared by the trigger and the menu
|
|
22
|
+
* items (EC-418). Fixed-width by construction: the `Pilcrow` icon is `size-3.5`
|
|
23
|
+
* (14px) and the heading badge is `w-3.5` (14px), so the active-style glyph never
|
|
24
|
+
* changes the trigger's width as the selection moves across styles.
|
|
25
|
+
*/
|
|
26
|
+
export function BlockStyleGlyph({ headingLevel }) {
|
|
27
|
+
const icon = blockStyleIcon(headingLevel);
|
|
28
|
+
return icon.kind === 'paragraph' ? (_jsx(Pilcrow, { className: "size-3.5" })) : (_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", icon.level] }));
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=block-style.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"block-style.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/block-style.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAEtC,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AA0BtD,kFAAkF;AAClF,MAAM,CAAC,MAAM,YAAY,GAAoC;IAC3D,EAAE,YAAY,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,YAAY,EAAE,EAAE;IACtD,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;IAChE,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;IAChE,EAAE,YAAY,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,EAAE;CACxD,CAAA;AASV,MAAM,UAAU,cAAc,CAAC,YAA2B;IACxD,OAAO,YAAY,IAAI,IAAI,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,YAAY,EAAE,CAAA;AAChG,CAAC;AAED,8FAA8F;AAC9F,MAAM,UAAU,eAAe,CAAC,YAA2B;IACzD,OAAO,YAAY,IAAI,IAAI;QACzB,CAAC,CAAC,SAAS,CAAC,oBAAoB,CAAC;QACjC,CAAC,CAAC,SAAS,CAAC,kBAAkB,EAAE,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC,CAAA;AAC5D,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,YAAY,EAAmC;IAC/E,MAAM,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC,CAAA;IACzC,OAAO,IAAI,CAAC,IAAI,KAAK,WAAW,CAAC,CAAC,CAAC,CACjC,KAAC,OAAO,IAAC,SAAS,EAAC,UAAU,GAAG,CACjC,CAAC,CAAC,CAAC,CACF,gBAAM,SAAS,EAAC,yCAAyC,kBAAG,IAAI,CAAC,KAAK,IAAQ,CAC/E,CAAA;AACH,CAAC","sourcesContent":["import { Pilcrow } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * Shared block-style vocabulary for the rich-text toolbar (EC-418).\n *\n * The block-style dropdown's TRIGGER and its MENU ITEMS must show the SAME\n * icon-per-style so they can never drift: paragraph → the lucide `Pilcrow`, a\n * heading → its compact `H{level}` badge. Declaring the mapping ONCE here —\n * consumed by both — makes that drift structurally impossible, the same pattern\n * `block-commands.ts` (EC-328) uses for the list/quote/code commands.\n *\n * This also underpins the EC-418 width-stability fix: the trigger renders the\n * ACTIVE style's fixed-width glyph instead of its variable-width localized label\n * (\"Absatz\" ≪ \"Überschrift 2\"), so caret movement across styles never reflows the\n * strip. The full localized labels appear only in the opened menu, and the trigger\n * keeps its accessible name via {@link blockStyleLabel} (an `aria-label`).\n */\n\n/** A block-style option: paragraph (`headingLevel: null`) or a heading level. */\nexport interface BlockStyleDescriptor {\n /** Heading level, or `null` for paragraph (the base body style). */\n headingLevel: number | null\n /** Apply this style on an already-`.focus()`ed chain. */\n chain: (chain: ReturnType<Editor['chain']>) => ReturnType<Editor['chain']>\n}\n\n/** The block styles the toolbar offers, in menu order (paragraph, then H1–H3). */\nexport const BLOCK_STYLES: readonly BlockStyleDescriptor[] = [\n { headingLevel: null, chain: (c) => c.setParagraph() },\n { headingLevel: 1, chain: (c) => c.toggleHeading({ level: 1 }) },\n { headingLevel: 2, chain: (c) => c.toggleHeading({ level: 2 }) },\n { headingLevel: 3, chain: (c) => c.toggleHeading({ level: 3 }) },\n] as const\n\n/**\n * The pure icon-per-style decision (EC-418) — the single source both the trigger\n * and the menu items derive their glyph from (via {@link BlockStyleGlyph}). Kept\n * data-only (no JSX) so it is unit-testable in a node env without a DOM.\n */\nexport type BlockStyleIcon = { kind: 'paragraph' } | { kind: 'heading'; level: number }\n\nexport function blockStyleIcon(headingLevel: number | null): BlockStyleIcon {\n return headingLevel == null ? { kind: 'paragraph' } : { kind: 'heading', level: headingLevel }\n}\n\n/** The localized label for a block style — the menu-item text and the trigger's a11y name. */\nexport function blockStyleLabel(headingLevel: number | null): string {\n return headingLevel == null\n ? translate('richText.paragraph')\n : translate('richText.heading', { level: headingLevel })\n}\n\n/**\n * The one glyph rendered for a block style, shared by the trigger and the menu\n * items (EC-418). Fixed-width by construction: the `Pilcrow` icon is `size-3.5`\n * (14px) and the heading badge is `w-3.5` (14px), so the active-style glyph never\n * changes the trigger's width as the selection moves across styles.\n */\nexport function BlockStyleGlyph({ headingLevel }: { headingLevel: number | null }) {\n const icon = blockStyleIcon(headingLevel)\n return icon.kind === 'paragraph' ? (\n <Pilcrow className=\"size-3.5\" />\n ) : (\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{icon.level}</span>\n )\n}\n"]}
|
|
@@ -3,7 +3,8 @@ import { useMemo, useState } from 'react';
|
|
|
3
3
|
import { Blocks } from 'lucide-react';
|
|
4
4
|
import { getStudioRegistry } from '@elytracms/studio/lib/registry';
|
|
5
5
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, Input, Stack, Text, } from '@elytracms/studio/ui';
|
|
6
|
-
import {
|
|
6
|
+
import { localizedStringVariants } from '@elytracms/core/cms-core';
|
|
7
|
+
import { localizedLabel, translate } from '@elytracms/studio/lib/i18n';
|
|
7
8
|
import { cn } from '@elytracms/studio/lib/utils';
|
|
8
9
|
import { listEmbeddable } from './insert.js';
|
|
9
10
|
/**
|
|
@@ -18,13 +19,13 @@ export function EmbedPicker({ open, onOpenChange, allow, onSelect }) {
|
|
|
18
19
|
const q = query.trim().toLowerCase();
|
|
19
20
|
if (!q)
|
|
20
21
|
return embeddable;
|
|
21
|
-
return embeddable.filter((m) =>
|
|
22
|
+
return embeddable.filter((m) => [m.id, ...localizedStringVariants(m.title)].some((s) => s.toLowerCase().includes(q)));
|
|
22
23
|
}, [embeddable, query]);
|
|
23
24
|
function choose(manifest) {
|
|
24
25
|
onSelect(manifest);
|
|
25
26
|
onOpenChange(false);
|
|
26
27
|
setQuery('');
|
|
27
28
|
}
|
|
28
|
-
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, 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: () => choose(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
|
|
29
|
+
return (_jsx(Dialog, { open: open, onOpenChange: onOpenChange, 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: () => choose(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: localizedLabel(manifest.title, manifest.id) }), _jsx("span", { className: "truncate text-xs text-muted-foreground", children: manifest.id })] })] }, manifest.id)))) })] })] }) }));
|
|
29
30
|
}
|
|
30
31
|
//# sourceMappingURL=embed-picker.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACzC,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAA;AAErC,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;
|
|
1
|
+
{"version":3,"file":"embed-picker.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/embed-picker.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useMemo, useState } from 'react'\nimport { Blocks } from 'lucide-react'\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 { localizedStringVariants } from '@elytracms/core/cms-core'\nimport { localizedLabel, translate } from '@elytracms/studio/lib/i18n'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { listEmbeddable } from './insert'\n\ninterface EmbedPickerProps {\n open: boolean\n onOpenChange: (open: boolean) => void\n /** The field's embed `allow` list (component ids). Empty/absent ⇒ any non-primitive. */\n allow?: string[]\n onSelect: (manifest: ComponentManifest) => void\n}\n\n/**\n * The component-embed picker dialog (EC-314/EC-189): a searchable list of the\n * components permitted for inline embedding. Rendered once at the editor level and\n * opened by the toolbar button or the `/embed` slash command.\n */\nexport function EmbedPicker({ open, onOpenChange, allow, onSelect }: EmbedPickerProps) {\n const [query, setQuery] = useState('')\n\n const embeddable = useMemo(\n () => listEmbeddable(allow, getStudioRegistry().registry.list()),\n [allow],\n )\n\n const results = useMemo(() => {\n const q = query.trim().toLowerCase()\n if (!q) return embeddable\n return embeddable.filter((m) =>\n [m.id, ...localizedStringVariants(m.title)].some((s) => s.toLowerCase().includes(q)),\n )\n }, [embeddable, query])\n\n function choose(manifest: ComponentManifest) {\n onSelect(manifest)\n onOpenChange(false)\n setQuery('')\n }\n\n return (\n <Dialog open={open} onOpenChange={onOpenChange}>\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={() => choose(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\">{localizedLabel(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"]}
|
|
@@ -17,6 +17,8 @@ import type { SlashCommandOptions } from './slash-command.js';
|
|
|
17
17
|
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
18
18
|
* atoms (asset image, embedded registry component) so existing content of those
|
|
19
19
|
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
20
|
+
* - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
|
|
21
|
+
* round-trips (an undeclared mark would be silently dropped on load).
|
|
20
22
|
*
|
|
21
23
|
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
22
24
|
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
@@ -2,6 +2,7 @@ import StarterKit from '@tiptap/starter-kit';
|
|
|
2
2
|
import { Placeholder } from '@tiptap/extensions';
|
|
3
3
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
4
|
import { RefLink } from './ref-link.js';
|
|
5
|
+
import { FontSize } from './font-size.js';
|
|
5
6
|
import { AssetImage } from './asset-image.js';
|
|
6
7
|
import { ComponentEmbed } from './component-embed.js';
|
|
7
8
|
import { createSlashCommand } from './slash-command.js';
|
|
@@ -22,6 +23,8 @@ import { createSlashCommand } from './slash-command.js';
|
|
|
22
23
|
* - {@link AssetImage} / {@link ComponentEmbed} declare the two reference-carrying
|
|
23
24
|
* atoms (asset image, embedded registry component) so existing content of those
|
|
24
25
|
* kinds survives a round-trip verbatim rather than being dropped by the schema.
|
|
26
|
+
* - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run
|
|
27
|
+
* round-trips (an undeclared mark would be silently dropped on load).
|
|
25
28
|
*
|
|
26
29
|
* No `allow` gating lives here — that governs *insertion* (the embed picker) and
|
|
27
30
|
* *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.
|
|
@@ -36,6 +39,7 @@ export function buildRichTextExtensions(options = {}) {
|
|
|
36
39
|
heading: { levels: [1, 2, 3, 4, 5, 6] },
|
|
37
40
|
}),
|
|
38
41
|
RefLink.configure({ openOnClick: false, autolink: true, linkOnPaste: true }),
|
|
42
|
+
FontSize,
|
|
39
43
|
AssetImage,
|
|
40
44
|
ComponentEmbed,
|
|
41
45
|
Placeholder.configure({ placeholder: translate('richText.placeholder') }),
|
|
@@ -1 +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;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD
|
|
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,QAAQ,EAAE,MAAM,aAAa,CAAA;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAA;AAC1C,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAClD,OAAO,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAA;AAGpD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,uBAAuB,CAAC,UAA2C,EAAE;IACnF,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,QAAQ;QACR,UAAU;QACV,cAAc;QACd,WAAW,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,SAAS,CAAC,sBAAsB,CAAC,EAAE,CAAC;QACzE,kBAAkB,CAAC,OAAO,CAAC,KAAK,IAAI,EAAE,CAAC;KACxC,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 { FontSize } from './font-size'\nimport { AssetImage } from './asset-image'\nimport { ComponentEmbed } from './component-embed'\nimport { createSlashCommand } from './slash-command'\nimport type { SlashCommandOptions } from './slash-command'\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 * - {@link FontSize} declares the semantic font-size mark (EC-411) so a sized run\n * round-trips (an undeclared mark would be silently dropped on load).\n *\n * No `allow` gating lives here — that governs *insertion* (the embed picker) and\n * *validation* (`validateRichTextEmbeds`), not which nodes the schema tolerates.\n *\n * `slash` wires the Notion-style `/` menu to the editor's shared image/embed pickers\n * (omit an action to hide that slash item).\n */\nexport function buildRichTextExtensions(options: { slash?: SlashCommandOptions } = {}): 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 FontSize,\n AssetImage,\n ComponentEmbed,\n Placeholder.configure({ placeholder: translate('richText.placeholder') }),\n createSlashCommand(options.slash ?? {}),\n ]\n}\n"]}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import type { RichTextFontSize } from '@elytracms/core/rich-text';
|
|
3
|
+
import type { MessageKey } from '@elytracms/studio/lib/i18n';
|
|
4
|
+
/**
|
|
5
|
+
* A font-size preset in the toolbar's dropdown (EC-411). `size: null` is the
|
|
6
|
+
* markless *normal* step — selecting it REMOVES the mark rather than storing a value.
|
|
7
|
+
*/
|
|
8
|
+
export interface FontSizePreset {
|
|
9
|
+
size: RichTextFontSize | null;
|
|
10
|
+
labelKey: MessageKey;
|
|
11
|
+
}
|
|
12
|
+
/** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */
|
|
13
|
+
export declare const FONT_SIZE_PRESETS: readonly FontSizePreset[];
|
|
14
|
+
/** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */
|
|
15
|
+
export declare function isPresetActive(preset: FontSizePreset, activeSize: RichTextFontSize | null): boolean;
|
|
16
|
+
/** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */
|
|
17
|
+
export declare function applyFontSizePreset(editor: Editor, size: RichTextFontSize | null): void;
|
|
18
|
+
interface FontSizeControlProps {
|
|
19
|
+
editor: Editor;
|
|
20
|
+
/** The active semantic step (from the toolbar state), or null for normal. */
|
|
21
|
+
activeSize: RichTextFontSize | null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The rich-text font-size control (EC-411). Width-stable under the EC-418 contract
|
|
25
|
+
* (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box
|
|
26
|
+
* as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels
|
|
27
|
+
* live only in the opened menu, never on the strip, so the control never resizes with
|
|
28
|
+
* selection. The trigger keeps an accessible name via an `aria-label`.
|
|
29
|
+
*/
|
|
30
|
+
export declare function FontSizeControl({ editor, activeSize }: FontSizeControlProps): import("react").JSX.Element;
|
|
31
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { ALargeSmall, Check, ChevronDown } from 'lucide-react';
|
|
3
|
+
import { FONT_SIZE_MARK_TYPE } from '@elytracms/core/rich-text';
|
|
4
|
+
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@elytracms/studio/ui';
|
|
5
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
6
|
+
import { translate } from '@elytracms/studio/lib/i18n';
|
|
7
|
+
import { RICH_TEXT_MENU_TRIGGER_CLASS } from './toolbar.js';
|
|
8
|
+
/** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */
|
|
9
|
+
export const FONT_SIZE_PRESETS = [
|
|
10
|
+
{ size: 'sm', labelKey: 'richText.fontSize.small' },
|
|
11
|
+
{ size: null, labelKey: 'richText.fontSize.normal' },
|
|
12
|
+
{ size: 'lg', labelKey: 'richText.fontSize.large' },
|
|
13
|
+
{ size: 'xl', labelKey: 'richText.fontSize.xlarge' },
|
|
14
|
+
];
|
|
15
|
+
/** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */
|
|
16
|
+
export function isPresetActive(preset, activeSize) {
|
|
17
|
+
return preset.size === activeSize;
|
|
18
|
+
}
|
|
19
|
+
/** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */
|
|
20
|
+
export function applyFontSizePreset(editor, size) {
|
|
21
|
+
const chain = editor.chain().focus();
|
|
22
|
+
if (size)
|
|
23
|
+
chain.setMark(FONT_SIZE_MARK_TYPE, { size }).run();
|
|
24
|
+
else
|
|
25
|
+
chain.unsetMark(FONT_SIZE_MARK_TYPE).run();
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The rich-text font-size control (EC-411). Width-stable under the EC-418 contract
|
|
29
|
+
* (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box
|
|
30
|
+
* as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels
|
|
31
|
+
* live only in the opened menu, never on the strip, so the control never resizes with
|
|
32
|
+
* selection. The trigger keeps an accessible name via an `aria-label`.
|
|
33
|
+
*/
|
|
34
|
+
export function FontSizeControl({ editor, activeSize }) {
|
|
35
|
+
return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: RICH_TEXT_MENU_TRIGGER_CLASS, "aria-label": translate('richText.fontSize.label'), title: translate('richText.fontSize.label'), onMouseDown: (e) => e.preventDefault(), children: [_jsx(ALargeSmall, { className: "size-3.5" }), _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-40", children: FONT_SIZE_PRESETS.map((preset) => {
|
|
36
|
+
const active = isPresetActive(preset, activeSize);
|
|
37
|
+
return (_jsxs(DropdownMenuItem, { onSelect: () => applyFontSizePreset(editor, preset.size), children: [_jsx(Check, { className: cn('size-3.5', !active && 'invisible') }), ' ', translate(preset.labelKey)] }, preset.size ?? 'normal'));
|
|
38
|
+
}) })] }));
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=font-size-control.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-size-control.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/font-size-control.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE9D,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAA;AAE/D,OAAO,EACL,MAAM,EACN,YAAY,EACZ,mBAAmB,EACnB,gBAAgB,EAChB,mBAAmB,GACpB,MAAM,sBAAsB,CAAA;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,6BAA6B,CAAA;AAChD,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD,OAAO,EAAE,4BAA4B,EAAE,MAAM,WAAW,CAAA;AAWxD,0FAA0F;AAC1F,MAAM,CAAC,MAAM,iBAAiB,GAA8B;IAC1D,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,yBAAyB,EAAE;IACnD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,0BAA0B,EAAE;IACpD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,yBAAyB,EAAE;IACnD,EAAE,IAAI,EAAE,IAAI,EAAE,QAAQ,EAAE,0BAA0B,EAAE;CACrD,CAAA;AAED,0FAA0F;AAC1F,MAAM,UAAU,cAAc,CAAC,MAAsB,EAAE,UAAmC;IACxF,OAAO,MAAM,CAAC,IAAI,KAAK,UAAU,CAAA;AACnC,CAAC;AAED,0FAA0F;AAC1F,MAAM,UAAU,mBAAmB,CAAC,MAAc,EAAE,IAA6B;IAC/E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAA;IACpC,IAAI,IAAI;QAAE,KAAK,CAAC,OAAO,CAAC,mBAAmB,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,EAAE,CAAA;;QACvD,KAAK,CAAC,SAAS,CAAC,mBAAmB,CAAC,CAAC,GAAG,EAAE,CAAA;AACjD,CAAC;AAQD;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,EAAE,MAAM,EAAE,UAAU,EAAwB;IAC1E,OAAO,CACL,MAAC,YAAY,eACX,KAAC,mBAAmB,IAAC,OAAO,kBAC1B,MAAC,MAAM,IACL,IAAI,EAAC,QAAQ,EACb,OAAO,EAAC,OAAO,EACf,IAAI,EAAC,IAAI,EACT,SAAS,EAAE,4BAA4B,gBAC3B,SAAS,CAAC,yBAAyB,CAAC,EAChD,KAAK,EAAE,SAAS,CAAC,yBAAyB,CAAC,EAC3C,WAAW,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,cAAc,EAAE,aAEtC,KAAC,WAAW,IAAC,SAAS,EAAC,UAAU,GAAG,EACpC,KAAC,WAAW,IAAC,SAAS,EAAC,mBAAmB,GAAG,IACtC,GACW,EACtB,KAAC,mBAAmB,IAAC,KAAK,EAAC,OAAO,EAAC,SAAS,EAAC,UAAU,YACpD,iBAAiB,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE;oBAChC,MAAM,MAAM,GAAG,cAAc,CAAC,MAAM,EAAE,UAAU,CAAC,CAAA;oBACjD,OAAO,CACL,MAAC,gBAAgB,IAEf,QAAQ,EAAE,GAAG,EAAE,CAAC,mBAAmB,CAAC,MAAM,EAAE,MAAM,CAAC,IAAI,CAAC,aAExD,KAAC,KAAK,IAAC,SAAS,EAAE,EAAE,CAAC,UAAU,EAAE,CAAC,MAAM,IAAI,WAAW,CAAC,GAAI,EAAC,GAAG,EAC/D,SAAS,CAAC,MAAM,CAAC,QAAQ,CAAC,KAJtB,MAAM,CAAC,IAAI,IAAI,QAAQ,CAKX,CACpB,CAAA;gBACH,CAAC,CAAC,GACkB,IACT,CAChB,CAAA;AACH,CAAC","sourcesContent":["import { ALargeSmall, Check, ChevronDown } from 'lucide-react'\nimport type { Editor } from '@tiptap/react'\nimport { FONT_SIZE_MARK_TYPE } from '@elytracms/core/rich-text'\nimport type { RichTextFontSize } from '@elytracms/core/rich-text'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n} from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport type { MessageKey } from '@elytracms/studio/lib/i18n'\nimport { RICH_TEXT_MENU_TRIGGER_CLASS } from './toolbar'\n\n/**\n * A font-size preset in the toolbar's dropdown (EC-411). `size: null` is the\n * markless *normal* step — selecting it REMOVES the mark rather than storing a value.\n */\nexport interface FontSizePreset {\n size: RichTextFontSize | null\n labelKey: MessageKey\n}\n\n/** The semantic scale the toolbar offers, ascending: small · normal · large · x-large. */\nexport const FONT_SIZE_PRESETS: readonly FontSizePreset[] = [\n { size: 'sm', labelKey: 'richText.fontSize.small' },\n { size: null, labelKey: 'richText.fontSize.normal' },\n { size: 'lg', labelKey: 'richText.fontSize.large' },\n { size: 'xl', labelKey: 'richText.fontSize.xlarge' },\n]\n\n/** Whether a preset is the currently-active step (normal ⇔ no mark ⇔ activeSize null). */\nexport function isPresetActive(preset: FontSizePreset, activeSize: RichTextFontSize | null): boolean {\n return preset.size === activeSize\n}\n\n/** Apply a preset on a focused chain: set the mark for a step, or unset it for normal. */\nexport function applyFontSizePreset(editor: Editor, size: RichTextFontSize | null): void {\n const chain = editor.chain().focus()\n if (size) chain.setMark(FONT_SIZE_MARK_TYPE, { size }).run()\n else chain.unsetMark(FONT_SIZE_MARK_TYPE).run()\n}\n\ninterface FontSizeControlProps {\n editor: Editor\n /** The active semantic step (from the toolbar state), or null for normal. */\n activeSize: RichTextFontSize | null\n}\n\n/**\n * The rich-text font-size control (EC-411). Width-stable under the EC-418 contract\n * (AD-4): the trigger shows a fixed-width icon (+ ChevronDown) in the SAME fixed box\n * as the block-style trigger ({@link RICH_TEXT_MENU_TRIGGER_CLASS}) — the size labels\n * live only in the opened menu, never on the strip, so the control never resizes with\n * selection. The trigger keeps an accessible name via an `aria-label`.\n */\nexport function FontSizeControl({ editor, activeSize }: FontSizeControlProps) {\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={RICH_TEXT_MENU_TRIGGER_CLASS}\n aria-label={translate('richText.fontSize.label')}\n title={translate('richText.fontSize.label')}\n onMouseDown={(e) => e.preventDefault()}\n >\n <ALargeSmall className=\"size-3.5\" />\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n {FONT_SIZE_PRESETS.map((preset) => {\n const active = isPresetActive(preset, activeSize)\n return (\n <DropdownMenuItem\n key={preset.size ?? 'normal'}\n onSelect={() => applyFontSizePreset(editor, preset.size)}\n >\n <Check className={cn('size-3.5', !active && 'invisible')} />{' '}\n {translate(preset.labelKey)}\n </DropdownMenuItem>\n )\n })}\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Mark } from '@tiptap/react';
|
|
2
|
+
/**
|
|
3
|
+
* The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic
|
|
4
|
+
* size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',
|
|
5
|
+
* attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary
|
|
6
|
+
* one-for-one so the document round-trips through `parseRichTextValue` / the delivery
|
|
7
|
+
* renderer with no format change (the same discipline as the rest of the extension
|
|
8
|
+
* set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.
|
|
9
|
+
*
|
|
10
|
+
* The mark carries no custom commands — the toolbar applies/removes it with the
|
|
11
|
+
* built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM
|
|
12
|
+
* rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`
|
|
13
|
+
* attribute + `rich-text-size-{size}` class), so the editing surface styles the steps
|
|
14
|
+
* with the same CSS hook the delivery output exposes (no inline pixel styles).
|
|
15
|
+
*/
|
|
16
|
+
export declare const FontSize: Mark<any, any>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Mark, mergeAttributes } from '@tiptap/react';
|
|
2
|
+
import { FONT_SIZE_DATA_ATTR, FONT_SIZE_MARK_TYPE, fontSizeClass, isRichTextFontSize, } from '@elytracms/core/rich-text';
|
|
3
|
+
/**
|
|
4
|
+
* The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic
|
|
5
|
+
* size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',
|
|
6
|
+
* attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary
|
|
7
|
+
* one-for-one so the document round-trips through `parseRichTextValue` / the delivery
|
|
8
|
+
* renderer with no format change (the same discipline as the rest of the extension
|
|
9
|
+
* set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.
|
|
10
|
+
*
|
|
11
|
+
* The mark carries no custom commands — the toolbar applies/removes it with the
|
|
12
|
+
* built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM
|
|
13
|
+
* rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`
|
|
14
|
+
* attribute + `rich-text-size-{size}` class), so the editing surface styles the steps
|
|
15
|
+
* with the same CSS hook the delivery output exposes (no inline pixel styles).
|
|
16
|
+
*/
|
|
17
|
+
export const FontSize = Mark.create({
|
|
18
|
+
name: FONT_SIZE_MARK_TYPE,
|
|
19
|
+
addAttributes() {
|
|
20
|
+
return {
|
|
21
|
+
size: {
|
|
22
|
+
default: null,
|
|
23
|
+
parseHTML: (element) => element.getAttribute(FONT_SIZE_DATA_ATTR),
|
|
24
|
+
renderHTML: (attributes) => {
|
|
25
|
+
const size = attributes.size;
|
|
26
|
+
if (!isRichTextFontSize(size))
|
|
27
|
+
return {};
|
|
28
|
+
return { [FONT_SIZE_DATA_ATTR]: size, class: fontSizeClass(size) };
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
parseHTML() {
|
|
34
|
+
return [{ tag: `span[${FONT_SIZE_DATA_ATTR}]` }];
|
|
35
|
+
},
|
|
36
|
+
renderHTML({ HTMLAttributes }) {
|
|
37
|
+
return ['span', mergeAttributes(HTMLAttributes), 0];
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
//# sourceMappingURL=font-size.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-size.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/font-size.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,MAAM,eAAe,CAAA;AACrD,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,aAAa,EACb,kBAAkB,GACnB,MAAM,2BAA2B,CAAA;AAElC;;;;;;;;;;;;;GAaG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC;IAClC,IAAI,EAAE,mBAAmB;IAEzB,aAAa;QACX,OAAO;YACL,IAAI,EAAE;gBACJ,OAAO,EAAE,IAAI;gBACb,SAAS,EAAE,CAAC,OAAO,EAAE,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC,mBAAmB,CAAC;gBACjE,UAAU,EAAE,CAAC,UAAU,EAAE,EAAE;oBACzB,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAA;oBAC5B,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC;wBAAE,OAAO,EAAE,CAAA;oBACxC,OAAO,EAAE,CAAC,mBAAmB,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAA;gBACpE,CAAC;aACF;SACF,CAAA;IACH,CAAC;IAED,SAAS;QACP,OAAO,CAAC,EAAE,GAAG,EAAE,QAAQ,mBAAmB,GAAG,EAAE,CAAC,CAAA;IAClD,CAAC;IAED,UAAU,CAAC,EAAE,cAAc,EAAE;QAC3B,OAAO,CAAC,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,EAAE,CAAC,CAAC,CAAA;IACrD,CAAC;CACF,CAAC,CAAA","sourcesContent":["import { Mark, mergeAttributes } from '@tiptap/react'\nimport {\n FONT_SIZE_DATA_ATTR,\n FONT_SIZE_MARK_TYPE,\n fontSizeClass,\n isRichTextFontSize,\n} from '@elytracms/core/rich-text'\n\n/**\n * The CMS font-size mark (EC-411) — a project-owned TipTap mark over the semantic\n * size model in `@elytracms/core/rich-text`. It stores exactly `{ type: 'fontSize',\n * attrs: { size } }` where `size ∈ sm/lg/xl`, matching the storage vocabulary\n * one-for-one so the document round-trips through `parseRichTextValue` / the delivery\n * renderer with no format change (the same discipline as the rest of the extension\n * set). *Normal = no mark*: the toolbar removes the mark rather than storing a value.\n *\n * The mark carries no custom commands — the toolbar applies/removes it with the\n * built-in `setMark('fontSize', { size })` / `unsetMark('fontSize')`. Its DOM\n * rendering mirrors the delivery renderer's hook exactly (a `data-rich-text-size`\n * attribute + `rich-text-size-{size}` class), so the editing surface styles the steps\n * with the same CSS hook the delivery output exposes (no inline pixel styles).\n */\nexport const FontSize = Mark.create({\n name: FONT_SIZE_MARK_TYPE,\n\n addAttributes() {\n return {\n size: {\n default: null,\n parseHTML: (element) => element.getAttribute(FONT_SIZE_DATA_ATTR),\n renderHTML: (attributes) => {\n const size = attributes.size\n if (!isRichTextFontSize(size)) return {}\n return { [FONT_SIZE_DATA_ATTR]: size, class: fontSizeClass(size) }\n },\n },\n }\n },\n\n parseHTML() {\n return [{ tag: `span[${FONT_SIZE_DATA_ATTR}]` }]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['span', mergeAttributes(HTMLAttributes), 0]\n },\n})\n"]}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Editor } from '@tiptap/react';
|
|
2
|
+
import type { RichTextFontSize } from '@elytracms/core/rich-text';
|
|
2
3
|
/** Snapshot of the active formatting state, recomputed reactively per selection. */
|
|
3
4
|
export interface ToolbarState {
|
|
4
5
|
bold: boolean;
|
|
@@ -12,6 +13,8 @@ export interface ToolbarState {
|
|
|
12
13
|
codeBlock: boolean;
|
|
13
14
|
paragraph: boolean;
|
|
14
15
|
headingLevel: number | null;
|
|
16
|
+
/** The active semantic font-size step, or null for the markless normal size (EC-411). */
|
|
17
|
+
fontSize: RichTextFontSize | null;
|
|
15
18
|
canUndo: boolean;
|
|
16
19
|
canRedo: boolean;
|
|
17
20
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { RICH_TEXT_FONT_SIZES } from '@elytracms/core/rich-text';
|
|
1
2
|
/**
|
|
2
3
|
* The toolbar's state when there is no live editor to read — every control
|
|
3
4
|
* inactive and both history actions disabled. Returned by
|
|
@@ -16,6 +17,7 @@ export const DISABLED_TOOLBAR_STATE = {
|
|
|
16
17
|
codeBlock: false,
|
|
17
18
|
paragraph: false,
|
|
18
19
|
headingLevel: null,
|
|
20
|
+
fontSize: null,
|
|
19
21
|
canUndo: false,
|
|
20
22
|
canRedo: false,
|
|
21
23
|
};
|
|
@@ -55,6 +57,7 @@ export function toolbarStateFromEditor(editor) {
|
|
|
55
57
|
codeBlock: editor.isActive('codeBlock'),
|
|
56
58
|
paragraph: editor.isActive('paragraph'),
|
|
57
59
|
headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,
|
|
60
|
+
fontSize: RICH_TEXT_FONT_SIZES.find((size) => editor.isActive('fontSize', { size })) ?? null,
|
|
58
61
|
canUndo: editor.can().undo(),
|
|
59
62
|
canRedo: editor.can().redo(),
|
|
60
63
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbar-state.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar-state.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"toolbar-state.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar-state.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAA;AAsBhE;;;;;GAKG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAiB;IAClD,IAAI,EAAE,KAAK;IACX,MAAM,EAAE,KAAK;IACb,SAAS,EAAE,KAAK;IAChB,MAAM,EAAE,KAAK;IACb,IAAI,EAAE,KAAK;IACX,UAAU,EAAE,KAAK;IACjB,WAAW,EAAE,KAAK;IAClB,UAAU,EAAE,KAAK;IACjB,SAAS,EAAE,KAAK;IAChB,SAAS,EAAE,KAAK;IAChB,YAAY,EAAE,IAAI;IAClB,QAAQ,EAAE,IAAI;IACd,OAAO,EAAE,KAAK;IACd,OAAO,EAAE,KAAK;CACf,CAAA;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAAiC;IACtE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,WAAW;QAAE,OAAO,sBAAsB,CAAA;IAChE,IAAI,CAAC;QACH,OAAO;YACL,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,MAAM,EAAE,MAAM,CAAC,QAAQ,CAAC,QAAQ,CAAC;YACjC,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC;YAC7B,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC;YAC3C,UAAU,EAAE,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC;YACzC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,SAAS,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC;YACvC,YAAY,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC,IAAI,IAAI;YAC9F,QAAQ,EAAE,oBAAoB,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI;YAC5F,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;YAC5B,OAAO,EAAE,MAAM,CAAC,GAAG,EAAE,CAAC,IAAI,EAAE;SAC7B,CAAA;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,sBAAsB,CAAA;IAC/B,CAAC;AACH,CAAC","sourcesContent":["import type { Editor } from '@tiptap/react'\nimport { RICH_TEXT_FONT_SIZES } from '@elytracms/core/rich-text'\nimport type { RichTextFontSize } from '@elytracms/core/rich-text'\n\n/** Snapshot of the active formatting state, recomputed reactively per selection. */\nexport interface ToolbarState {\n bold: boolean\n italic: boolean\n underline: boolean\n strike: boolean\n code: boolean\n bulletList: boolean\n orderedList: boolean\n blockquote: boolean\n codeBlock: boolean\n paragraph: boolean\n headingLevel: number | null\n /** The active semantic font-size step, or null for the markless normal size (EC-411). */\n fontSize: RichTextFontSize | null\n canUndo: boolean\n canRedo: boolean\n}\n\n/**\n * The toolbar's state when there is no live editor to read — every control\n * inactive and both history actions disabled. Returned by\n * {@link toolbarStateFromEditor} for a missing / not-yet-initialized / destroyed\n * editor so the toolbar renders inert instead of throwing.\n */\nexport const DISABLED_TOOLBAR_STATE: ToolbarState = {\n bold: false,\n italic: false,\n underline: false,\n strike: false,\n code: false,\n bulletList: false,\n orderedList: false,\n blockquote: false,\n codeBlock: false,\n paragraph: false,\n headingLevel: null,\n fontSize: null,\n canUndo: false,\n canRedo: false,\n}\n\n/**\n * Project a TipTap editor into the toolbar's active-state shape — NULL-SAFE, and\n * the pure function the toolbar's `useEditorState` selector delegates to.\n *\n * `useEditorState`'s selector runs through `useSyncExternalStoreWithSelector`, so\n * React can invoke it against an editor that is missing, not yet initialized, or\n * already DESTROYED — e.g. during React 19 concurrent teardown, or when the\n * EC-322 lazy/Suspense boundary unmounts the editor subtree and `useEditor`'s\n * cleanup has already called `editor.destroy()` while the external store fires the\n * selector one last time. TipTap v3 nulls the editor's `commandManager` (and\n * `editorView`) on destroy, so `editor.can()` then throws\n * `Cannot read properties of null (reading 'can')` — the published-package crash\n * Konditorei hit (`editor.isActive(...)` returns stale state and does NOT throw,\n * which is why the reported stack points specifically at `.can`).\n *\n * Guarding on `isDestroyed` (safe on a destroyed editor — it reads\n * `editorView?.isDestroyed ?? true`) short-circuits before any command access; a\n * defensive `try/catch` covers any other pre-init window. The result is the\n * disabled shape rather than a throw, independent of mount/unmount timing.\n */\nexport function toolbarStateFromEditor(editor: Editor | null | undefined): ToolbarState {\n if (!editor || editor.isDestroyed) return DISABLED_TOOLBAR_STATE\n try {\n return {\n bold: editor.isActive('bold'),\n italic: editor.isActive('italic'),\n underline: editor.isActive('underline'),\n strike: editor.isActive('strike'),\n code: editor.isActive('code'),\n bulletList: editor.isActive('bulletList'),\n orderedList: editor.isActive('orderedList'),\n blockquote: editor.isActive('blockquote'),\n codeBlock: editor.isActive('codeBlock'),\n paragraph: editor.isActive('paragraph'),\n headingLevel: [1, 2, 3, 4, 5, 6].find((l) => editor.isActive('heading', { level: l })) ?? null,\n fontSize: RICH_TEXT_FONT_SIZES.find((size) => editor.isActive('fontSize', { size })) ?? null,\n canUndo: editor.can().undo(),\n canRedo: editor.can().redo(),\n }\n } catch {\n return DISABLED_TOOLBAR_STATE\n }\n}\n"]}
|
|
@@ -11,6 +11,19 @@ import type { LinkTarget } from './link-control.js';
|
|
|
11
11
|
* Pinned by `toolbar-layout.test.ts`.
|
|
12
12
|
*/
|
|
13
13
|
export declare const RICH_TEXT_TOOLBAR_CLASS = "h-auto min-h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5 py-1";
|
|
14
|
+
/**
|
|
15
|
+
* The block-style dropdown trigger's class (EC-418). The load-bearing part is the
|
|
16
|
+
* FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its
|
|
17
|
+
* variable-width localized label — "Absatz" ≪ "Überschrift 2"), so caret movement
|
|
18
|
+
* across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped
|
|
19
|
+
* strip beneath the cursor (the reported German-locale defect). `justify-between`
|
|
20
|
+
* seats the glyph and the ChevronDown in that fixed box. Every other control on the
|
|
21
|
+
* strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose
|
|
22
|
+
* active state only recolors (a `secondary` border sits inside the box-border square,
|
|
23
|
+
* never resizing it) — so this trigger was the sole selection-variable width.
|
|
24
|
+
* Pinned by `toolbar-layout.test.ts`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const RICH_TEXT_MENU_TRIGGER_CLASS = "h-7 w-12 justify-between gap-1 px-2";
|
|
14
27
|
interface RichTextToolbarProps {
|
|
15
28
|
editor: Editor;
|
|
16
29
|
/** Internal-link destinations across the workspace (empty ⇒ external-only). */
|