@elytracms/studio 0.0.64 → 0.0.66
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/account/self-account-view-source.js +9 -2
- package/dist/components/account/self-account-view-source.js.map +1 -1
- package/dist/components/app-views/default-modules.d.ts +28 -1
- package/dist/components/app-views/default-modules.js +47 -2
- package/dist/components/app-views/default-modules.js.map +1 -1
- 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/auth-context.js +32 -1
- package/dist/components/auth-context.js.map +1 -1
- package/dist/components/builder/blocks-model.d.ts +11 -0
- package/dist/components/builder/blocks-model.js +30 -10
- package/dist/components/builder/blocks-model.js.map +1 -1
- package/dist/components/builder/canvas-chrome-css.js +90 -8
- package/dist/components/builder/canvas-chrome-css.js.map +1 -1
- package/dist/components/builder/canvas-dnd.js +4 -2
- package/dist/components/builder/canvas-dnd.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.js +61 -12
- 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 +97 -5
- 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 +25 -5
- 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 +24 -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/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 +57 -1
- package/dist/components/builder/spacing-handles.js +142 -26
- package/dist/components/builder/spacing-handles.js.map +1 -1
- package/dist/components/cms/asset-field.js +21 -5
- package/dist/components/cms/asset-field.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/bulk-actions.d.ts +22 -3
- package/dist/components/cms/bulk-actions.js +27 -3
- package/dist/components/cms/bulk-actions.js.map +1 -1
- package/dist/components/cms/cms-store.js +19 -0
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/detail-editor.js +18 -19
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/prop-fields.js +23 -16
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/publish-clean.d.ts +21 -0
- package/dist/components/cms/publish-clean.js +30 -0
- package/dist/components/cms/publish-clean.js.map +1 -0
- 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/cms/use-live-url.d.ts +24 -1
- package/dist/components/cms/use-live-url.js +26 -4
- package/dist/components/cms/use-live-url.js.map +1 -1
- package/dist/components/cms/use-publish-controller.js +8 -6
- package/dist/components/cms/use-publish-controller.js.map +1 -1
- package/dist/components/collection-view/cms-collection-view-source.js +15 -7
- package/dist/components/collection-view/cms-collection-view-source.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.js +1 -1
- 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/auth/auth-adapter.d.ts +22 -0
- package/dist/lib/auth/auth-adapter.js.map +1 -1
- package/dist/lib/auth/convex-auth-adapter.d.ts +42 -1
- package/dist/lib/auth/convex-auth-adapter.js +69 -0
- package/dist/lib/auth/convex-auth-adapter.js.map +1 -1
- package/dist/lib/auth/dev-auto-login-adapter.d.ts +2 -1
- package/dist/lib/auth/dev-auto-login-adapter.js +6 -0
- package/dist/lib/auth/dev-auto-login-adapter.js.map +1 -1
- package/dist/lib/auth/index.d.ts +1 -1
- package/dist/lib/auth/index.js.map +1 -1
- package/dist/lib/auth/local-auth-adapter.d.ts +2 -1
- package/dist/lib/auth/local-auth-adapter.js +7 -0
- package/dist/lib/auth/local-auth-adapter.js.map +1 -1
- package/dist/lib/auth-errors.d.ts +45 -3
- package/dist/lib/auth-errors.js +46 -5
- package/dist/lib/auth-errors.js.map +1 -1
- package/dist/lib/clipboard/paste-target.js +5 -2
- package/dist/lib/clipboard/paste-target.js.map +1 -1
- package/dist/lib/dnd/drop-rules.d.ts +13 -0
- package/dist/lib/dnd/drop-rules.js +15 -0
- package/dist/lib/dnd/drop-rules.js.map +1 -1
- package/dist/lib/i18n/messages.de.js +19 -0
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +16 -0
- package/dist/lib/i18n/messages.en.js +24 -0
- package/dist/lib/i18n/messages.en.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.js +6 -3
- 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/dist/lib/session-expiry.d.ts +19 -6
- package/dist/lib/session-expiry.js +13 -1
- package/dist/lib/session-expiry.js.map +1 -1
- package/dist/lib/session-revalidation.d.ts +81 -0
- package/dist/lib/session-revalidation.js +94 -0
- package/dist/lib/session-revalidation.js.map +1 -0
- package/package.json +2 -2
- package/src/styles/studio.css +13 -0
|
@@ -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). */
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useEditorState } from '@tiptap/react';
|
|
3
|
-
import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, Maximize2, Minimize2,
|
|
3
|
+
import { Blocks, Bold, ChevronDown, Code, ImagePlus, Italic, Maximize2, Minimize2, Redo2, Strikethrough, Underline, Undo2, } from 'lucide-react';
|
|
4
4
|
import { Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Toolbar, ToolbarGroup, ToolbarSeparator, } from '@elytracms/studio/ui';
|
|
5
5
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
6
|
import { ToolbarButton } from './toolbar-button.js';
|
|
7
7
|
import { LinkControl } from './link-control.js';
|
|
8
8
|
import { toolbarStateFromEditor } from './toolbar-state.js';
|
|
9
9
|
import { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands.js';
|
|
10
|
+
import { BLOCK_STYLES, BlockStyleGlyph, blockStyleLabel } from './block-style.js';
|
|
11
|
+
import { FontSizeControl } from './font-size-control.js';
|
|
10
12
|
/**
|
|
11
13
|
* The toolbar container classes (EC-391). The load-bearing part is the height
|
|
12
14
|
* contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many
|
|
@@ -18,6 +20,19 @@ import { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands.js';
|
|
|
18
20
|
* Pinned by `toolbar-layout.test.ts`.
|
|
19
21
|
*/
|
|
20
22
|
export 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';
|
|
23
|
+
/**
|
|
24
|
+
* The block-style dropdown trigger's class (EC-418). The load-bearing part is the
|
|
25
|
+
* FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its
|
|
26
|
+
* variable-width localized label — "Absatz" ≪ "Überschrift 2"), so caret movement
|
|
27
|
+
* across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped
|
|
28
|
+
* strip beneath the cursor (the reported German-locale defect). `justify-between`
|
|
29
|
+
* seats the glyph and the ChevronDown in that fixed box. Every other control on the
|
|
30
|
+
* strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose
|
|
31
|
+
* active state only recolors (a `secondary` border sits inside the box-border square,
|
|
32
|
+
* never resizing it) — so this trigger was the sole selection-variable width.
|
|
33
|
+
* Pinned by `toolbar-layout.test.ts`.
|
|
34
|
+
*/
|
|
35
|
+
export const RICH_TEXT_MENU_TRIGGER_CLASS = 'h-7 w-12 justify-between gap-1 px-2';
|
|
21
36
|
/**
|
|
22
37
|
* The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that
|
|
23
38
|
* reads its active/enabled state reactively from the editor (`useEditorState`) so a
|
|
@@ -34,9 +49,6 @@ export function RichTextToolbar({ editor, targets, onOpenImage, onOpenEmbed, exp
|
|
|
34
49
|
editor,
|
|
35
50
|
selector: ({ editor }) => toolbarStateFromEditor(editor),
|
|
36
51
|
});
|
|
37
|
-
|
|
38
|
-
? translate('richText.heading', { level: state.headingLevel })
|
|
39
|
-
: translate('richText.paragraph');
|
|
40
|
-
return (_jsxs(Toolbar, { className: RICH_TEXT_TOOLBAR_CLASS, children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: "h-7 gap-1 px-2 font-medium", onMouseDown: (e) => e.preventDefault(), children: [styleLabel, _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsxs(DropdownMenuContent, { align: "start", className: "min-w-40", children: [_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().setParagraph().run(), children: [_jsx(Pilcrow, { className: "size-3.5" }), " ", translate('richText.paragraph')] }), [1, 2, 3].map((level) => (_jsxs(DropdownMenuItem, { onSelect: () => editor.chain().focus().toggleHeading({ level }).run(), children: [_jsxs("span", { className: "w-3.5 text-center text-xs font-semibold", children: ["H", level] }), ' ', translate('richText.heading', { level })] }, level)))] })] }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: BLOCK_COMMANDS.map((cmd) => (_jsx(ToolbarButton, { active: isBlockCommandActive(cmd, state), label: translate(cmd.labelKey), onClick: () => cmd.chain(editor.chain().focus()).run(), children: _jsx(cmd.icon, { className: "size-3.5" }) }, cmd.key))) }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] }), onToggleExpand ? (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: _jsx(ToolbarButton, { active: expanded, label: expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand'), onClick: onToggleExpand, children: expanded ? _jsx(Minimize2, { className: "size-3.5" }) : _jsx(Maximize2, { className: "size-3.5" }) }) })] })) : null] }));
|
|
52
|
+
return (_jsxs(Toolbar, { className: RICH_TEXT_TOOLBAR_CLASS, children: [_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsxs(Button, { type: "button", variant: "ghost", size: "sm", className: RICH_TEXT_MENU_TRIGGER_CLASS, "aria-label": blockStyleLabel(state.headingLevel), onMouseDown: (e) => e.preventDefault(), children: [_jsx(BlockStyleGlyph, { headingLevel: state.headingLevel }), _jsx(ChevronDown, { className: "size-3 opacity-60" })] }) }), _jsx(DropdownMenuContent, { align: "start", className: "min-w-40", children: BLOCK_STYLES.map((style) => (_jsxs(DropdownMenuItem, { onSelect: () => style.chain(editor.chain().focus()).run(), children: [_jsx(BlockStyleGlyph, { headingLevel: style.headingLevel }), ' ', blockStyleLabel(style.headingLevel)] }, style.headingLevel ?? 'paragraph'))) })] }), _jsx(FontSizeControl, { editor: editor, activeSize: state.fontSize }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: state.bold, label: translate('richText.bold'), onClick: () => editor.chain().focus().toggleBold().run(), children: _jsx(Bold, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.italic, label: translate('richText.italic'), onClick: () => editor.chain().focus().toggleItalic().run(), children: _jsx(Italic, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.underline, label: translate('richText.underline'), onClick: () => editor.chain().focus().toggleUnderline().run(), children: _jsx(Underline, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.strike, label: translate('richText.strike'), onClick: () => editor.chain().focus().toggleStrike().run(), children: _jsx(Strikethrough, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: state.code, label: translate('richText.code'), onClick: () => editor.chain().focus().toggleCode().run(), children: _jsx(Code, { className: "size-3.5" }) })] }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: BLOCK_COMMANDS.map((cmd) => (_jsx(ToolbarButton, { active: isBlockCommandActive(cmd, state), label: translate(cmd.labelKey), onClick: () => cmd.chain(editor.chain().focus()).run(), children: _jsx(cmd.icon, { className: "size-3.5" }) }, cmd.key))) }), _jsx(ToolbarSeparator, {}), _jsxs(ToolbarGroup, { children: [_jsx(LinkControl, { editor: editor, targets: targets }), onOpenImage ? (_jsx(ToolbarButton, { label: translate('richText.image.add'), onClick: onOpenImage, children: _jsx(ImagePlus, { className: "size-3.5" }) })) : null, onOpenEmbed ? (_jsx(ToolbarButton, { label: translate('richText.embed.add'), onClick: onOpenEmbed, children: _jsx(Blocks, { className: "size-3.5" }) })) : null] }), _jsx("div", { className: "flex-1" }), _jsxs(ToolbarGroup, { children: [_jsx(ToolbarButton, { active: false, disabled: !state.canUndo, label: translate('richText.undo'), onClick: () => editor.chain().focus().undo().run(), children: _jsx(Undo2, { className: "size-3.5" }) }), _jsx(ToolbarButton, { active: false, disabled: !state.canRedo, label: translate('richText.redo'), onClick: () => editor.chain().focus().redo().run(), children: _jsx(Redo2, { className: "size-3.5" }) })] }), onToggleExpand ? (_jsxs(_Fragment, { children: [_jsx(ToolbarSeparator, {}), _jsx(ToolbarGroup, { children: _jsx(ToolbarButton, { active: expanded, label: expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand'), onClick: onToggleExpand, children: expanded ? _jsx(Minimize2, { className: "size-3.5" }) : _jsx(Maximize2, { className: "size-3.5" }) }) })] })) : null] }));
|
|
41
53
|
}
|
|
42
54
|
//# sourceMappingURL=toolbar.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n Maximize2,\n Minimize2,\n Pilcrow,\n Redo2,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\nimport { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands'\n\n/**\n * The toolbar container classes (EC-391). The load-bearing part is the height\n * contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many\n * controls wrap at narrow inspector widths the strip GROWS and reserves its real\n * layout height — the editable content always starts below it. A fixed height plus\n * `flex-wrap` was the bug: wrapped rows overflowed the locked box and painted over\n * the prose, so the buttons were un-hittable. `py-1` only bites once wrapped (a\n * single row is governed by `min-h-10` + `items-center`), padding the stacked rows.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_TOOLBAR_CLASS =\n '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'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n /** EC-388: whether the editor is currently in its expanded (sheet) surface. */\n expanded?: boolean\n /** EC-388: toggle the expanded editing surface (omitted ⇒ no expand affordance). */\n onToggleExpand?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({\n editor,\n targets,\n onOpenImage,\n onOpenEmbed,\n expanded = false,\n onToggleExpand,\n}: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n const styleLabel =\n state.headingLevel != null\n ? translate('richText.heading', { level: state.headingLevel })\n : translate('richText.paragraph')\n\n return (\n <Toolbar className={RICH_TEXT_TOOLBAR_CLASS}>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className=\"h-7 gap-1 px-2 font-medium\"\n onMouseDown={(e) => e.preventDefault()}\n >\n {styleLabel}\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n <DropdownMenuItem onSelect={() => editor.chain().focus().setParagraph().run()}>\n <Pilcrow className=\"size-3.5\" /> {translate('richText.paragraph')}\n </DropdownMenuItem>\n {([1, 2, 3] as const).map((level) => (\n <DropdownMenuItem\n key={level}\n onSelect={() => editor.chain().focus().toggleHeading({ level }).run()}\n >\n <span className=\"w-3.5 text-center text-xs font-semibold\">H{level}</span>{' '}\n {translate('richText.heading', { level })}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n {/* The shared table (EC-328) — the SAME descriptors `buildSlashItems`\n derives its bulletList/orderedList/blockquote/codeBlock/divider items from. */}\n {BLOCK_COMMANDS.map((cmd) => (\n <ToolbarButton\n key={cmd.key}\n active={isBlockCommandActive(cmd, state)}\n label={translate(cmd.labelKey)}\n onClick={() => cmd.chain(editor.chain().focus()).run()}\n >\n <cmd.icon className=\"size-3.5\" />\n </ToolbarButton>\n ))}\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n {/* EC-388: the expand/collapse affordance — moves with the toolbar into the\n expanded sheet (where it reads as \"collapse\") and back to the inline\n editor (where it reads as \"expand\"). Same editor instance, same value\n flow; no separate save/cancel. */}\n {onToggleExpand ? (\n <>\n <ToolbarSeparator />\n <ToolbarGroup>\n <ToolbarButton\n active={expanded}\n label={\n expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand')\n }\n onClick={onToggleExpand}\n >\n {expanded ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </ToolbarButton>\n </ToolbarGroup>\n </>\n ) : null}\n </Toolbar>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"toolbar.js","sourceRoot":"","sources":["../../../../src/components/cms/rich-text/toolbar.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useEditorState } from '@tiptap/react'\nimport type { Editor } from '@tiptap/react'\nimport {\n Blocks,\n Bold,\n ChevronDown,\n Code,\n ImagePlus,\n Italic,\n Maximize2,\n Minimize2,\n Redo2,\n Strikethrough,\n Underline,\n Undo2,\n} from 'lucide-react'\nimport {\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuTrigger,\n Toolbar,\n ToolbarGroup,\n ToolbarSeparator,\n} from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { ToolbarButton } from './toolbar-button'\nimport { LinkControl } from './link-control'\nimport type { LinkTarget } from './link-control'\nimport { toolbarStateFromEditor } from './toolbar-state'\nimport { BLOCK_COMMANDS, isBlockCommandActive } from './block-commands'\nimport { BLOCK_STYLES, BlockStyleGlyph, blockStyleLabel } from './block-style'\nimport { FontSizeControl } from './font-size-control'\n\n/**\n * The toolbar container classes (EC-391). The load-bearing part is the height\n * contract: `h-auto min-h-10` (NOT a fixed `h-10`/`h-11`) so that when the many\n * controls wrap at narrow inspector widths the strip GROWS and reserves its real\n * layout height — the editable content always starts below it. A fixed height plus\n * `flex-wrap` was the bug: wrapped rows overflowed the locked box and painted over\n * the prose, so the buttons were un-hittable. `py-1` only bites once wrapped (a\n * single row is governed by `min-h-10` + `items-center`), padding the stacked rows.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_TOOLBAR_CLASS =\n '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'\n\n/**\n * The block-style dropdown trigger's class (EC-418). The load-bearing part is the\n * FIXED width (`w-12`): the trigger shows the active style's icon glyph (not its\n * variable-width localized label — \"Absatz\" ≪ \"Überschrift 2\"), so caret movement\n * across Absatz/H1/H2/H3 can never change the trigger's width and reflow the wrapped\n * strip beneath the cursor (the reported German-locale defect). `justify-between`\n * seats the glyph and the ChevronDown in that fixed box. Every other control on the\n * strip is already a fixed `size-7` square (`ToolbarButton`, `LinkControl`) whose\n * active state only recolors (a `secondary` border sits inside the box-border square,\n * never resizing it) — so this trigger was the sole selection-variable width.\n * Pinned by `toolbar-layout.test.ts`.\n */\nexport const RICH_TEXT_MENU_TRIGGER_CLASS = 'h-7 w-12 justify-between gap-1 px-2'\n\ninterface RichTextToolbarProps {\n editor: Editor\n /** Internal-link destinations across the workspace (empty ⇒ external-only). */\n targets: LinkTarget[]\n /** Open the shared image picker (omitted ⇒ no image button). */\n onOpenImage?: () => void\n /** Open the shared component-embed picker (omitted ⇒ no embed button). */\n onOpenEmbed?: () => void\n /** EC-388: whether the editor is currently in its expanded (sheet) surface. */\n expanded?: boolean\n /** EC-388: toggle the expanded editing surface (omitted ⇒ no expand affordance). */\n onToggleExpand?: () => void\n}\n\n/**\n * The CMS rich-text toolbar (EC-314) — a compact, Notion-grade control strip that\n * reads its active/enabled state reactively from the editor (`useEditorState`) so a\n * pressed control always mirrors the caret. Every button prevents the default\n * mousedown so clicking it never steals the editor selection.\n */\nexport function RichTextToolbar({\n editor,\n targets,\n onOpenImage,\n onOpenEmbed,\n expanded = false,\n onToggleExpand,\n}: RichTextToolbarProps) {\n // The selector delegates to a pure, NULL-SAFE projection: `useEditorState` runs\n // it through an external store that React can fire against a destroyed/pre-init\n // editor (concurrent teardown, the EC-322 lazy boundary), where `editor.can()`\n // would throw on TipTap v3's nulled `commandManager`. `toolbarStateFromEditor`\n // returns the disabled shape in that window instead of crashing the toolbar.\n const state = useEditorState({\n editor,\n selector: ({ editor }) => toolbarStateFromEditor(editor),\n })\n\n return (\n <Toolbar className={RICH_TEXT_TOOLBAR_CLASS}>\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n className={RICH_TEXT_MENU_TRIGGER_CLASS}\n // The visible label moved into the opened menu (EC-418) — the trigger keeps\n // an accessible name for the ACTIVE style from the same catalog keys.\n aria-label={blockStyleLabel(state.headingLevel)}\n onMouseDown={(e) => e.preventDefault()}\n >\n <BlockStyleGlyph headingLevel={state.headingLevel} />\n <ChevronDown className=\"size-3 opacity-60\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"start\" className=\"min-w-40\">\n {BLOCK_STYLES.map((style) => (\n <DropdownMenuItem\n key={style.headingLevel ?? 'paragraph'}\n onSelect={() => style.chain(editor.chain().focus()).run()}\n >\n <BlockStyleGlyph headingLevel={style.headingLevel} />{' '}\n {blockStyleLabel(style.headingLevel)}\n </DropdownMenuItem>\n ))}\n </DropdownMenuContent>\n </DropdownMenu>\n\n <FontSizeControl editor={editor} activeSize={state.fontSize} />\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <ToolbarButton\n active={state.bold}\n label={translate('richText.bold')}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.italic}\n label={translate('richText.italic')}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.underline}\n label={translate('richText.underline')}\n onClick={() => editor.chain().focus().toggleUnderline().run()}\n >\n <Underline className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.strike}\n label={translate('richText.strike')}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={state.code}\n label={translate('richText.code')}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n {/* The shared table (EC-328) — the SAME descriptors `buildSlashItems`\n derives its bulletList/orderedList/blockquote/codeBlock/divider items from. */}\n {BLOCK_COMMANDS.map((cmd) => (\n <ToolbarButton\n key={cmd.key}\n active={isBlockCommandActive(cmd, state)}\n label={translate(cmd.labelKey)}\n onClick={() => cmd.chain(editor.chain().focus()).run()}\n >\n <cmd.icon className=\"size-3.5\" />\n </ToolbarButton>\n ))}\n </ToolbarGroup>\n\n <ToolbarSeparator />\n\n <ToolbarGroup>\n <LinkControl editor={editor} targets={targets} />\n {onOpenImage ? (\n <ToolbarButton label={translate('richText.image.add')} onClick={onOpenImage}>\n <ImagePlus className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n {onOpenEmbed ? (\n <ToolbarButton label={translate('richText.embed.add')} onClick={onOpenEmbed}>\n <Blocks className=\"size-3.5\" />\n </ToolbarButton>\n ) : null}\n </ToolbarGroup>\n\n <div className=\"flex-1\" />\n\n <ToolbarGroup>\n <ToolbarButton\n active={false}\n disabled={!state.canUndo}\n label={translate('richText.undo')}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 className=\"size-3.5\" />\n </ToolbarButton>\n <ToolbarButton\n active={false}\n disabled={!state.canRedo}\n label={translate('richText.redo')}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 className=\"size-3.5\" />\n </ToolbarButton>\n </ToolbarGroup>\n\n {/* EC-388: the expand/collapse affordance — moves with the toolbar into the\n expanded sheet (where it reads as \"collapse\") and back to the inline\n editor (where it reads as \"expand\"). Same editor instance, same value\n flow; no separate save/cancel. */}\n {onToggleExpand ? (\n <>\n <ToolbarSeparator />\n <ToolbarGroup>\n <ToolbarButton\n active={expanded}\n label={\n expanded ? translate('richText.expand.collapse') : translate('richText.expand.expand')\n }\n onClick={onToggleExpand}\n >\n {expanded ? <Minimize2 className=\"size-3.5\" /> : <Maximize2 className=\"size-3.5\" />}\n </ToolbarButton>\n </ToolbarGroup>\n </>\n ) : null}\n </Toolbar>\n )\n}\n"]}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef, Locale } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CollectionDef, DocumentUrl, Locale, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
|
|
3
3
|
/** The header's live-link descriptor: an `href` (open live) + a title, or a muted
|
|
4
4
|
* reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */
|
|
@@ -23,6 +23,29 @@ export declare function resolveHeaderLive(input: {
|
|
|
23
23
|
loading: boolean;
|
|
24
24
|
published: boolean;
|
|
25
25
|
}): HeaderLive | undefined;
|
|
26
|
+
/**
|
|
27
|
+
* EC-384 — derive a published document's live URL against the project's STORED
|
|
28
|
+
* config routes MERGED with the kit's implicit `/` → first-published-page
|
|
29
|
+
* fallback (EC-368/EC-187) — the exact same `mergeRouteRecords`/
|
|
30
|
+
* `fallbackHomeRoutes` pair the delivery resolver merges through
|
|
31
|
+
* (`@elytracms/core/cms-core`), so the studio and delivery can never disagree
|
|
32
|
+
* about a fallback-addressed home page's URL. Pure (no React), so it is
|
|
33
|
+
* unit-tested directly rather than through the hook. `siblings` doubles as the
|
|
34
|
+
* fallback's candidate pool: when `collection` is the `page` collection it IS
|
|
35
|
+
* the published `page` pool the fallback rule needs; for any other collection
|
|
36
|
+
* the derived fallback routes simply address no route of `collection` (a
|
|
37
|
+
* harmless no-op — `urlForDocument` only matches routes whose `document.collection`
|
|
38
|
+
* equals the target collection).
|
|
39
|
+
*/
|
|
40
|
+
export declare function resolveDocumentLiveUrl(input: {
|
|
41
|
+
routes: readonly RouteRecord[];
|
|
42
|
+
localeConfig: LocaleConfig;
|
|
43
|
+
frontendUrl: string | undefined;
|
|
44
|
+
collection: CollectionDef;
|
|
45
|
+
doc: CmsDocument;
|
|
46
|
+
siblings: readonly CmsDocument[];
|
|
47
|
+
locale: Locale;
|
|
48
|
+
}): DocumentUrl | undefined;
|
|
26
49
|
/**
|
|
27
50
|
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
28
51
|
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import { urlForDocument } from '@elytracms/core/cms-core';
|
|
2
|
+
import { fallbackHomeRoutes, mergeRouteRecords, urlForDocument } from '@elytracms/core/cms-core';
|
|
3
3
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
4
4
|
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
5
5
|
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
@@ -25,6 +25,24 @@ export function resolveHeaderLive(input) {
|
|
|
25
25
|
: translate('cms.editor.setFrontendUrlHint'),
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* EC-384 — derive a published document's live URL against the project's STORED
|
|
30
|
+
* config routes MERGED with the kit's implicit `/` → first-published-page
|
|
31
|
+
* fallback (EC-368/EC-187) — the exact same `mergeRouteRecords`/
|
|
32
|
+
* `fallbackHomeRoutes` pair the delivery resolver merges through
|
|
33
|
+
* (`@elytracms/core/cms-core`), so the studio and delivery can never disagree
|
|
34
|
+
* about a fallback-addressed home page's URL. Pure (no React), so it is
|
|
35
|
+
* unit-tested directly rather than through the hook. `siblings` doubles as the
|
|
36
|
+
* fallback's candidate pool: when `collection` is the `page` collection it IS
|
|
37
|
+
* the published `page` pool the fallback rule needs; for any other collection
|
|
38
|
+
* the derived fallback routes simply address no route of `collection` (a
|
|
39
|
+
* harmless no-op — `urlForDocument` only matches routes whose `document.collection`
|
|
40
|
+
* equals the target collection).
|
|
41
|
+
*/
|
|
42
|
+
export function resolveDocumentLiveUrl(input) {
|
|
43
|
+
const routes = mergeRouteRecords(input.routes, fallbackHomeRoutes(input.siblings, input.localeConfig));
|
|
44
|
+
return urlForDocument({ routes, frontendUrl: input.frontendUrl, localeConfig: input.localeConfig }, input.collection, input.doc, { siblings: input.siblings, locale: input.locale });
|
|
45
|
+
}
|
|
28
46
|
/**
|
|
29
47
|
* EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned
|
|
30
48
|
* published snapshot + its published ancestors), never the draft, so an unpublished
|
|
@@ -38,11 +56,15 @@ export function useLiveUrl(input) {
|
|
|
38
56
|
const publishedDoc = publishedDocs.byId.get(doc.id);
|
|
39
57
|
if (!publishedDoc)
|
|
40
58
|
return undefined;
|
|
41
|
-
return
|
|
59
|
+
return resolveDocumentLiveUrl({
|
|
42
60
|
routes: resolveDeploymentRoutes(),
|
|
43
|
-
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
44
61
|
localeConfig: source.localeConfig,
|
|
45
|
-
|
|
62
|
+
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
63
|
+
collection,
|
|
64
|
+
doc: publishedDoc,
|
|
65
|
+
siblings: [...publishedDocs.byId.values()],
|
|
66
|
+
locale,
|
|
67
|
+
});
|
|
46
68
|
}, [publishedDocs, collection, doc.id, locale, source.localeConfig]);
|
|
47
69
|
const headerLive = resolveHeaderLive({
|
|
48
70
|
routableCollection,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"use-live-url.js","sourceRoot":"","sources":["../../../src/components/cms/use-live-url.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAA;AAUhG,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AACtD,OAAO,EAAE,4BAA4B,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAA;AACvG,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAA;AASrF;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAAC,KAKjC;IACC,IAAI,CAAC,KAAK,CAAC,kBAAkB;QAAE,OAAO,SAAS,CAAA;IAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,EAAE,CAAC;QACxB,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,CAAC,wBAAwB,EAAE,EAAE,IAAI,EAAE,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC,EAAE,CAAA;IAC/G,CAAC;IACD,OAAO;QACL,KAAK,EAAE,KAAK,CAAC,OAAO;YAClB,CAAC,CAAC,SAAS,CAAC,2BAA2B,CAAC;YACxC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS;gBAChB,CAAC,CAAC,SAAS,CAAC,8BAA8B,CAAC;gBAC3C,CAAC,CAAC,SAAS,CAAC,+BAA+B,CAAC;KACjD,CAAA;AACH,CAAC;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CAAC,KAQtC;IACC,MAAM,MAAM,GAAG,iBAAiB,CAAC,KAAK,CAAC,MAAM,EAAE,kBAAkB,CAAC,KAAK,CAAC,QAAQ,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC,CAAA;IACtG,OAAO,cAAc,CACnB,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,CAAC,WAAW,EAAE,YAAY,EAAE,KAAK,CAAC,YAAY,EAAE,EAC5E,KAAK,CAAC,UAAU,EAChB,KAAK,CAAC,GAAG,EACT,EAAE,QAAQ,EAAE,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CACnD,CAAA;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAC,KAO1B;IACC,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IAChF,MAAM,aAAa,GAAG,qBAAqB,CAAC,MAAM,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;IAC5E,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,EAAE;QAC3B,MAAM,YAAY,GAAG,aAAa,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACnD,IAAI,CAAC,YAAY;YAAE,OAAO,SAAS,CAAA;QACnC,OAAO,sBAAsB,CAAC;YAC5B,MAAM,EAAE,uBAAuB,EAAE;YACjC,YAAY,EAAE,MAAM,CAAC,YAAY;YACjC,WAAW,EAAE,4BAA4B,EAAE;YAC3C,UAAU;YACV,GAAG,EAAE,YAAY;YACjB,QAAQ,EAAE,CAAC,GAAG,aAAa,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC1C,MAAM;SACP,CAAC,CAAA;IACJ,CAAC,EAAE,CAAC,aAAa,EAAE,UAAU,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC,CAAA;IAEpE,MAAM,UAAU,GAAG,iBAAiB,CAAC;QACnC,kBAAkB;QAClB,OAAO;QACP,OAAO,EAAE,aAAa,CAAC,MAAM,KAAK,SAAS;QAC3C,SAAS;KACV,CAAC,CAAA;IACF,OAAO,EAAE,UAAU,EAAE,CAAA;AACvB,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { fallbackHomeRoutes, mergeRouteRecords, urlForDocument } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentUrl,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { translate } from '@elytracms/studio/lib/i18n'\nimport { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents'\n\n/** The header's live-link descriptor: an `href` (open live) + a title, or a muted\n * reason-only title while it isn't openable (loading / publish-first / no frontendUrl). */\nexport interface HeaderLive {\n href?: string\n title: string\n}\n\n/**\n * 0.0.42 (round 3) — the header composed-URL row's open-live affordance, as a pure\n * decision. Only a routable collection shows the row; its leaf opens the live page\n * when the doc is published AND a frontendUrl is set (an absolute `href`),\n * otherwise it is a muted icon whose tooltip explains WHY (still loading, publish\n * first, or set the frontendUrl). The draft-derived path itself updates as you\n * type — only the OPEN-live action is published-gated, preserving draft/live.\n */\nexport function resolveHeaderLive(input: {\n routableCollection: boolean\n liveUrl: { href?: string; path: string } | undefined\n loading: boolean\n published: boolean\n}): HeaderLive | undefined {\n if (!input.routableCollection) return undefined\n if (input.liveUrl?.href) {\n return { href: input.liveUrl.href, title: translate('cms.editor.openLiveUrl', { path: input.liveUrl.path }) }\n }\n return {\n title: input.loading\n ? translate('cms.editor.loadingLiveUrl')\n : !input.published\n ? translate('cms.editor.publishToViewLive')\n : translate('cms.editor.setFrontendUrlHint'),\n }\n}\n\n/**\n * EC-384 — derive a published document's live URL against the project's STORED\n * config routes MERGED with the kit's implicit `/` → first-published-page\n * fallback (EC-368/EC-187) — the exact same `mergeRouteRecords`/\n * `fallbackHomeRoutes` pair the delivery resolver merges through\n * (`@elytracms/core/cms-core`), so the studio and delivery can never disagree\n * about a fallback-addressed home page's URL. Pure (no React), so it is\n * unit-tested directly rather than through the hook. `siblings` doubles as the\n * fallback's candidate pool: when `collection` is the `page` collection it IS\n * the published `page` pool the fallback rule needs; for any other collection\n * the derived fallback routes simply address no route of `collection` (a\n * harmless no-op — `urlForDocument` only matches routes whose `document.collection`\n * equals the target collection).\n */\nexport function resolveDocumentLiveUrl(input: {\n routes: readonly RouteRecord[]\n localeConfig: LocaleConfig\n frontendUrl: string | undefined\n collection: CollectionDef\n doc: CmsDocument\n siblings: readonly CmsDocument[]\n locale: Locale\n}): DocumentUrl | undefined {\n const routes = mergeRouteRecords(input.routes, fallbackHomeRoutes(input.siblings, input.localeConfig))\n return urlForDocument(\n { routes, frontendUrl: input.frontendUrl, localeConfig: input.localeConfig },\n input.collection,\n input.doc,\n { siblings: input.siblings, locale: input.locale },\n )\n}\n\n/**\n * EC-205 / EC-216 — the LIVE URL derives strictly from PUBLISHED data (the pinned\n * published snapshot + its published ancestors), never the draft, so an unpublished\n * slug/parent edit can't leak into the live URL. Resolves the header's live-link\n * descriptor for the composed-URL row; `undefined` for a non-routable collection.\n */\nexport function useLiveUrl(input: {\n source: CollectionViewSource\n collection: CollectionDef\n doc: CmsDocument\n locale: Locale\n published: boolean\n routableCollection: boolean\n}): { headerLive: HeaderLive | undefined } {\n const { source, collection, doc, locale, published, routableCollection } = input\n const publishedDocs = usePublishedDocuments(source.projectId, collection.id)\n const liveUrl = useMemo(() => {\n const publishedDoc = publishedDocs.byId.get(doc.id)\n if (!publishedDoc) return undefined\n return resolveDocumentLiveUrl({\n routes: resolveDeploymentRoutes(),\n localeConfig: source.localeConfig,\n frontendUrl: resolveDeploymentFrontendUrl(),\n collection,\n doc: publishedDoc,\n siblings: [...publishedDocs.byId.values()],\n locale,\n })\n }, [publishedDocs, collection, doc.id, locale, source.localeConfig])\n\n const headerLive = resolveHeaderLive({\n routableCollection,\n liveUrl,\n loading: publishedDocs.status === 'loading',\n published,\n })\n return { headerLive }\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from 'react';
|
|
2
2
|
import { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router';
|
|
3
|
-
import {
|
|
3
|
+
import { cleanHiddenForPublish } from './publish-clean.js';
|
|
4
4
|
import { toast } from '@elytracms/studio/ui';
|
|
5
5
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
6
6
|
/**
|
|
@@ -37,11 +37,13 @@ export function usePublishController(input) {
|
|
|
37
37
|
// dialog so a successful publish returns to the list. Cleared on cancel or failure.
|
|
38
38
|
const closeAfterPublishRef = useRef(false);
|
|
39
39
|
const doPublish = useCallback(async () => {
|
|
40
|
-
// EC-273
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
|
|
40
|
+
// EC-273/EC-394 via the SHARED clean step (EC-425, also run by bulk publish):
|
|
41
|
+
// clean conditionally-hidden field values AND component props from the draft
|
|
42
|
+
// AT PUBLISH TIME — into the draft AND the snapshot, so both go clean
|
|
43
|
+
// together. Also captures the latest edit if Publish fires inside the
|
|
44
|
+
// autosave debounce window; the serial persister runs the upsert before the
|
|
45
|
+
// publish so the snapshot is this draft.
|
|
46
|
+
const cleaned = cleanHiddenForPublish(collection, draft);
|
|
45
47
|
if (cleaned !== draft)
|
|
46
48
|
setDraft(cleaned);
|
|
47
49
|
if (cleaned !== draft || dirty)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"use-publish-controller.js","sourceRoot":"","sources":["../../../src/components/cms/use-publish-controller.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AACrD,OAAO,EAAE,oBAAoB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAGxF,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAA;AACvD,OAAO,EAAE,KAAK,EAAE,MAAM,sBAAsB,CAAA;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAItD;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAC,KAK3B;IACC,MAAM,QAAQ,GAAG,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,YAAY,IAAI,KAAK,CAAC,YAAY,KAAK,SAAS,CAAA;IAC1F,MAAM,KAAK,GAAG,KAAK,CAAC,YAAY;QAC9B,CAAC,CAAC,SAAS,CAAC,mCAAmC,CAAC;QAChD,CAAC,CAAC,KAAK,CAAC,SAAS;YACf,CAAC,CAAC,SAAS,CACP,KAAK,CAAC,UAAU,KAAK,CAAC;gBACpB,CAAC,CAAC,mCAAmC;gBACrC,CAAC,CAAC,qCAAqC,EACzC,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,CAC5B;YACH,CAAC,CAAC,SAAS,CAAA;IACf,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAA;AAC5B,CAAC;AAgBD;;;;;;GAMG;AACH,MAAM,UAAU,oBAAoB,CAAC,KASpC;IACC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,kBAAkB,EAAE,GAAG,KAAK,CAAA;IACnG,MAAM,QAAQ,GAAG,WAAW,EAAE,CAAA;IAC9B,MAAM,CAAC,YAAY,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAe,MAAM,CAAC,CAAA;IACtE,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAA;IAC3D,oFAAoF;IACpF,oFAAoF;IACpF,MAAM,oBAAoB,GAAG,MAAM,CAAC,KAAK,CAAC,CAAA;IAE1C,MAAM,SAAS,GAAG,WAAW,CAAC,KAAK,IAAI,EAAE;QACvC,8EAA8E;QAC9E,6EAA6E;QAC7E,sEAAsE;QACtE,sEAAsE;QACtE,4EAA4E;QAC5E,yCAAyC;QACzC,MAAM,OAAO,GAAG,qBAAqB,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;QACxD,IAAI,OAAO,KAAK,KAAK;YAAE,QAAQ,CAAC,OAAO,CAAC,CAAA;QACxC,IAAI,OAAO,KAAK,KAAK,IAAI,KAAK;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACpD,eAAe,CAAC,SAAS,CAAC,CAAA;QAC1B,IAAI,CAAC;YACH,MAAM,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;YACxC,eAAe,CAAC,SAAS,CAAC,CAAA;YAC1B,UAAU,CAAC,GAAG,EAAE,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,CAAA;YAC/C,4EAA4E;YAC5E,8EAA8E;YAC9E,IAAI,oBAAoB,CAAC,OAAO,EAAE,CAAC;gBACjC,oBAAoB,CAAC,OAAO,GAAG,KAAK,CAAA;gBACpC,KAAK,QAAQ,CAAC;oBACZ,EAAE,EAAE,oBAAoB;oBACxB,MAAM,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,QAAQ,IAAI,UAAU,CAAC,KAAK,EAAE,EAAE;iBACrE,CAAC,CAAA;YACJ,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,oBAAoB,CAAC,OAAO,GAAG,KAAK,CAAA;YACpC,eAAe,CAAC,MAAM,CAAC,CAAA;YACvB,KAAK,CAAC,KAAK,CACT,KAAK,YAAY,KAAK;gBACpB,CAAC,CAAC,SAAS,CAAC,gCAAgC,EAAE,EAAE,KAAK,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC;gBACvE,CAAC,CAAC,SAAS,CAAC,0BAA0B,CAAC,CAC1C,CAAA;QACH,CAAC;IACH,CAAC,EAAE,CAAC,UAAU,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAA;IAE7E,6EAA6E;IAC7E,kFAAkF;IAClF,MAAM,aAAa,GAAG,WAAW,CAAC,GAAG,EAAE;QACrC,IAAI,kBAAkB,EAAE,CAAC;YACvB,iBAAiB,CAAC,IAAI,CAAC,CAAA;YACvB,OAAM;QACR,CAAC;QACD,KAAK,SAAS,EAAE,CAAA;IAClB,CAAC,EAAE,CAAC,kBAAkB,EAAE,SAAS,CAAC,CAAC,CAAA;IAEnC,MAAM,qBAAqB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7C,oBAAoB,CAAC,OAAO,GAAG,IAAI,CAAA;QACnC,aAAa,EAAE,CAAA;IACjB,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAA;IAEnB,MAAM,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;QACtC,iBAAiB,CAAC,KAAK,CAAC,CAAA;QACxB,KAAK,SAAS,EAAE,CAAA;IAClB,CAAC,EAAE,CAAC,SAAS,CAAC,CAAC,CAAA;IAEf,MAAM,oBAAoB,GAAG,WAAW,CAAC,GAAG,EAAE;QAC5C,iBAAiB,CAAC,KAAK,CAAC,CAAA;QACxB,oBAAoB,CAAC,OAAO,GAAG,KAAK,CAAA;IACtC,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,OAAO;QACL,YAAY;QACZ,cAAc;QACd,aAAa;QACb,qBAAqB;QACrB,cAAc;QACd,oBAAoB;KACrB,CAAA;AACH,CAAC","sourcesContent":["import { useCallback, useRef, useState } from 'react'\nimport { STUDIO_PROJECT_ROUTE, studioPath, useNavigate } from '@elytracms/studio/router'\nimport type { CmsDocument, CollectionDef, DocumentRef } from '@elytracms/core/cms-core'\nimport type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { cleanHiddenForPublish } from './publish-clean'\nimport { toast } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\nexport type PublishState = 'idle' | 'pending' | 'success'\n\n/**\n * EC-268 / 0.0.39 — the split Publish button's shared gate, computed once so the\n * primary \"Publish\" and the \"Publish & close\" item can never diverge. A pure\n * decision (directly testable): the Published perspective is read-only (switch to\n * Draft to publish), schema errors block (with a count-aware hint), and an\n * in-flight publish disables re-entry.\n */\nexport function publishGate(input: {\n hasErrors: boolean\n viewReadOnly: boolean\n publishState: PublishState\n errorCount: number\n}): { disabled: boolean; title: string | undefined } {\n const disabled = input.hasErrors || input.viewReadOnly || input.publishState === 'pending'\n const title = input.viewReadOnly\n ? translate('cms.editor.switchToDraftToPublish')\n : input.hasErrors\n ? translate(\n input.errorCount === 1\n ? 'cms.editor.fixIssuesToPublish.one'\n : 'cms.editor.fixIssuesToPublish.other',\n { count: input.errorCount },\n )\n : undefined\n return { disabled, title }\n}\n\nexport interface PublishController {\n publishState: PublishState\n /** Is the confirm-through dialog (unpublished hard refs) open? */\n publishConfirm: boolean\n /** Publish (opening the confirm-through dialog first if there are unpublished refs). */\n handlePublish: () => void\n /** Publish and, on success, return to the rail's list (0.0.39 \"Publish & close\"). */\n handlePublishAndClose: () => void\n /** Confirm-through: publish despite the unpublished hard references. */\n confirmPublish: () => void\n /** Dismiss the confirm-through dialog (also clears a pending \"& close\" intent). */\n cancelPublishConfirm: () => void\n}\n\n/**\n * EC-268 / EC-300 / 0.0.39 — the publish controller. Owns the publish indicator\n * state, the EC-300 confirm-through guard (unpublished hard references would render\n * empty on the live site, so publishing confirms rather than blocks), and the\n * 0.0.39 \"Publish & close\" navigation (remembered across the confirm dialog via a\n * ref, so a successful publish returns to the rail-owned list).\n */\nexport function usePublishController(input: {\n source: CollectionViewSource\n docRef: DocumentRef\n collection: CollectionDef\n projectId: string\n draft: CmsDocument\n setDraft: (doc: CmsDocument) => void\n dirty: boolean\n hasUnpublishedRefs: boolean\n}): PublishController {\n const { source, docRef, collection, projectId, draft, setDraft, dirty, hasUnpublishedRefs } = input\n const navigate = useNavigate()\n const [publishState, setPublishState] = useState<PublishState>('idle')\n const [publishConfirm, setPublishConfirm] = useState(false)\n // The split-button \"Publish & close\" intent — remembered across the confirm-through\n // dialog so a successful publish returns to the list. Cleared on cancel or failure.\n const closeAfterPublishRef = useRef(false)\n\n const doPublish = useCallback(async () => {\n // EC-273/EC-394 via the SHARED clean step (EC-425, also run by bulk publish):\n // clean conditionally-hidden field values AND component props from the draft\n // AT PUBLISH TIME — into the draft AND the snapshot, so both go clean\n // together. Also captures the latest edit if Publish fires inside the\n // autosave debounce window; the serial persister runs the upsert before the\n // publish so the snapshot is this draft.\n const cleaned = cleanHiddenForPublish(collection, draft)\n if (cleaned !== draft) setDraft(cleaned)\n if (cleaned !== draft || dirty) source.save(cleaned)\n setPublishState('pending')\n try {\n await source.publishing?.publish(docRef)\n setPublishState('success')\n setTimeout(() => setPublishState('idle'), 1800)\n // \"Publish & close\": return to the list once the publish landed (never on a\n // failure). Rail-owned `listPath`, falling back to the project index (AD-19).\n if (closeAfterPublishRef.current) {\n closeAfterPublishRef.current = false\n void navigate({\n to: STUDIO_PROJECT_ROUTE,\n params: { projectId, _splat: source.listPath ?? studioPath.index() },\n })\n }\n } catch (error) {\n closeAfterPublishRef.current = false\n setPublishState('idle')\n toast.error(\n error instanceof Error\n ? translate('cms.editor.publishFailedReason', { error: error.message })\n : translate('cms.editor.publishFailed'),\n )\n }\n }, [collection, draft, setDraft, dirty, source, docRef, navigate, projectId])\n\n // Gate publish on the confirm-through dialog when there are unpublished hard\n // references; schema errors already disable the button, so they never reach here.\n const handlePublish = useCallback(() => {\n if (hasUnpublishedRefs) {\n setPublishConfirm(true)\n return\n }\n void doPublish()\n }, [hasUnpublishedRefs, doPublish])\n\n const handlePublishAndClose = useCallback(() => {\n closeAfterPublishRef.current = true\n handlePublish()\n }, [handlePublish])\n\n const confirmPublish = useCallback(() => {\n setPublishConfirm(false)\n void doPublish()\n }, [doPublish])\n\n const cancelPublishConfirm = useCallback(() => {\n setPublishConfirm(false)\n closeAfterPublishRef.current = false\n }, [])\n\n return {\n publishState,\n publishConfirm,\n handlePublish,\n handlePublishAndClose,\n confirmPublish,\n cancelPublishConfirm,\n }\n}\n"]}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { useCallback, useMemo } from 'react';
|
|
2
|
-
import { isHierarchical
|
|
2
|
+
import { isHierarchical } from '@elytracms/core/cms-core';
|
|
3
3
|
import { studioPath } from '@elytracms/studio/router';
|
|
4
4
|
import { resolveDeploymentFrontendUrl, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
|
|
5
5
|
import { useCms } from '../cms/cms-store.js';
|
|
6
6
|
import { useOptionalAssets } from '../assets/assets-store.js';
|
|
7
7
|
import { usePublishedDocuments } from '@elytracms/studio/lib/cms/published-documents';
|
|
8
|
+
import { resolveDocumentLiveUrl } from '../cms/use-live-url.js';
|
|
8
9
|
/**
|
|
9
10
|
* Assemble a {@link CollectionViewSource} for a `cmsDocuments` collection from the
|
|
10
11
|
* ambient `CmsStore` + its side contexts (assets, published pool) + the
|
|
@@ -28,18 +29,25 @@ export function useCmsCollectionViewSource(collectionId) {
|
|
|
28
29
|
// Live-URL column: the published pool × deployment routes, memoised exactly as
|
|
29
30
|
// the table did — derived from PUBLISHED snapshots, never draft rows (EC-205).
|
|
30
31
|
const isRoutable = useMemo(() => resolveDeploymentRoutes().some((route) => route.document?.collection === collectionId), [collectionId]);
|
|
32
|
+
// EC-384: `resolveDocumentLiveUrl` merges the stored routes with the kit's
|
|
33
|
+
// fallback-home rule (same as the document editor's header affordance,
|
|
34
|
+
// `use-live-url.ts`) — otherwise a fallback-addressed home page showed no URL
|
|
35
|
+
// in this column even though the live site serves it.
|
|
31
36
|
const liveUrlMap = useMemo(() => {
|
|
32
37
|
if (!collection || !isRoutable)
|
|
33
38
|
return null;
|
|
34
|
-
const cfg = {
|
|
35
|
-
routes: resolveDeploymentRoutes(),
|
|
36
|
-
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
37
|
-
localeConfig: cms.localeConfig,
|
|
38
|
-
};
|
|
39
39
|
const siblings = [...publishedDocs.byId.values()];
|
|
40
40
|
const map = new Map();
|
|
41
41
|
for (const published of siblings) {
|
|
42
|
-
const url =
|
|
42
|
+
const url = resolveDocumentLiveUrl({
|
|
43
|
+
routes: resolveDeploymentRoutes(),
|
|
44
|
+
frontendUrl: resolveDeploymentFrontendUrl(),
|
|
45
|
+
localeConfig: cms.localeConfig,
|
|
46
|
+
collection,
|
|
47
|
+
doc: published,
|
|
48
|
+
siblings,
|
|
49
|
+
locale: cms.activeLocale,
|
|
50
|
+
});
|
|
43
51
|
if (url)
|
|
44
52
|
map.set(published.id, url);
|
|
45
53
|
}
|