@elytracms/studio 0.0.39 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/cms/collection-table.js +13 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/detail-editor.js +116 -72
- package/dist/components/cms/detail-editor.js.map +1 -1
- package/dist/components/cms/details-pane.d.ts +21 -8
- package/dist/components/cms/details-pane.js +31 -11
- package/dist/components/cms/details-pane.js.map +1 -1
- package/dist/components/cms/field-control.js +21 -12
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/prop-fields.js +25 -1
- package/dist/components/cms/prop-fields.js.map +1 -1
- package/dist/components/cms/rich-text/component-embed.d.ts +7 -2
- package/dist/components/cms/rich-text/component-embed.js +60 -5
- package/dist/components/cms/rich-text/component-embed.js.map +1 -1
- package/dist/components/cms/rich-text/editor.js +24 -5
- package/dist/components/cms/rich-text/editor.js.map +1 -1
- package/dist/components/cms/rich-text/embed-inspector-context.d.ts +50 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js +12 -0
- package/dist/components/cms/rich-text/embed-inspector-context.js.map +1 -0
- package/dist/components/cms/rich-text/embed-inspector.d.ts +25 -0
- package/dist/components/cms/rich-text/embed-inspector.js +48 -0
- package/dist/components/cms/rich-text/embed-inspector.js.map +1 -0
- package/dist/components/cms/rich-text/embed-picker.d.ts +0 -2
- package/dist/components/cms/rich-text/embed-picker.js +0 -4
- package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
- package/dist/components/cms/rich-text/embed-props.d.ts +7 -0
- package/dist/components/cms/rich-text/embed-props.js +9 -0
- package/dist/components/cms/rich-text/embed-props.js.map +1 -1
- package/dist/components/cms/rich-text/insert.d.ts +8 -2
- package/dist/components/cms/rich-text/insert.js +32 -3
- package/dist/components/cms/rich-text/insert.js.map +1 -1
- package/dist/components/cms/rich-text/link-control.js +3 -1
- package/dist/components/cms/rich-text/link-control.js.map +1 -1
- package/dist/components/cms/rich-text/slash-command.d.ts +23 -6
- package/dist/components/cms/rich-text/slash-command.js +31 -23
- package/dist/components/cms/rich-text/slash-command.js.map +1 -1
- package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
- package/dist/components/cms/rich-text/toolbar-state.js +66 -0
- package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
- package/dist/components/cms/rich-text/toolbar.js +7 -15
- package/dist/components/cms/rich-text/toolbar.js.map +1 -1
- package/dist/components/cms/version-compare.js +51 -22
- package/dist/components/cms/version-compare.js.map +1 -1
- package/dist/components/components/component-preview.d.ts +36 -0
- package/dist/components/components/component-preview.js +157 -5
- package/dist/components/components/component-preview.js.map +1 -1
- package/dist/lib/i18n/messages.en.js +18 -3
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,21 +1,32 @@
|
|
|
1
|
-
import type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core';
|
|
1
|
+
import type { CmsDocument, CollectionDef, FieldDef, LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
|
|
2
2
|
import type { ReactNode } from 'react';
|
|
3
|
+
/** 0.0.39 — routing context the Details pane needs to compose the document URL row. */
|
|
4
|
+
export interface DetailsUrlContext {
|
|
5
|
+
projectId: string;
|
|
6
|
+
routes: readonly RouteRecord[];
|
|
7
|
+
localeConfig: LocaleConfig;
|
|
8
|
+
locale: string;
|
|
9
|
+
}
|
|
3
10
|
/**
|
|
4
|
-
* EC-284 + EC-285 — document-level metadata pane (the right
|
|
5
|
-
* optional regions:
|
|
11
|
+
* EC-284 + EC-285 (+ 0.0.39 URL row) — document-level metadata pane (the right
|
|
12
|
+
* inspector). Up to three optional regions:
|
|
6
13
|
*
|
|
7
|
-
* 1. **
|
|
14
|
+
* 1. **URL** (0.0.39): the full composed path as its own row — the EC-315 "hybrid
|
|
15
|
+
* split" moves the composed URL out of the cramped slug field and up to the
|
|
16
|
+
* document, where a URL is a first-class property. Ancestor-DOCUMENT segments
|
|
17
|
+
* link to their editors; static route segments are plain text.
|
|
18
|
+
* 2. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
8
19
|
* (the main menu, the footer menu…) instead of which exact entry it is — shown
|
|
9
20
|
* only when the collection declares alias slots.
|
|
10
|
-
*
|
|
21
|
+
* 3. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
|
|
11
22
|
* author, publish date…) — document-level properties an editor sets *about* the
|
|
12
23
|
* document rather than as body content. Rendered via the detail editor's own
|
|
13
24
|
* field-row renderer, so they behave identically to a content field.
|
|
14
25
|
*
|
|
15
|
-
*
|
|
16
|
-
* tabs. The empty state shows only when there is
|
|
26
|
+
* All three are identity/metadata, not content, so they live here — off the content
|
|
27
|
+
* tabs. The empty state shows only when there is nothing at all to display.
|
|
17
28
|
*/
|
|
18
|
-
export declare function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, }: {
|
|
29
|
+
export declare function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, urlContext, }: {
|
|
19
30
|
collection: CollectionDef;
|
|
20
31
|
doc: CmsDocument;
|
|
21
32
|
/** The other documents in this collection (to show which slots are taken). */
|
|
@@ -26,4 +37,6 @@ export declare function DetailsPane({ collection, doc, siblings, readOnly, field
|
|
|
26
37
|
/** Render one field row — shared with the content groups so behaviour matches. */
|
|
27
38
|
renderField: (field: FieldDef) => ReactNode;
|
|
28
39
|
onAliasChange: (alias: string | undefined) => void;
|
|
40
|
+
/** Routing context for the composed URL row (0.0.39); omitted for non-routable rails. */
|
|
41
|
+
urlContext?: DetailsUrlContext;
|
|
29
42
|
}): import("react").JSX.Element;
|
|
@@ -1,30 +1,50 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { declaredAliases } from '@elytracms/core/cms-core';
|
|
3
|
-
import {
|
|
2
|
+
import { declaredAliases, resolveUrlSegments } from '@elytracms/core/cms-core';
|
|
3
|
+
import { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router';
|
|
4
|
+
import { Row, Select, Separator, Stack, Text } from '@elytracms/studio/ui';
|
|
5
|
+
import { cn } from '@elytracms/studio/lib/utils';
|
|
4
6
|
import { translate } from '@elytracms/studio/lib/i18n';
|
|
5
7
|
/**
|
|
6
|
-
* EC-284 + EC-285 — document-level metadata pane (the right
|
|
7
|
-
* optional regions:
|
|
8
|
+
* EC-284 + EC-285 (+ 0.0.39 URL row) — document-level metadata pane (the right
|
|
9
|
+
* inspector). Up to three optional regions:
|
|
8
10
|
*
|
|
9
|
-
* 1. **
|
|
11
|
+
* 1. **URL** (0.0.39): the full composed path as its own row — the EC-315 "hybrid
|
|
12
|
+
* split" moves the composed URL out of the cramped slug field and up to the
|
|
13
|
+
* document, where a URL is a first-class property. Ancestor-DOCUMENT segments
|
|
14
|
+
* link to their editors; static route segments are plain text.
|
|
15
|
+
* 2. **Slot** (EC-284): a stable, named handle the site uses to find this document
|
|
10
16
|
* (the main menu, the footer menu…) instead of which exact entry it is — shown
|
|
11
17
|
* only when the collection declares alias slots.
|
|
12
|
-
*
|
|
18
|
+
* 3. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
|
|
13
19
|
* author, publish date…) — document-level properties an editor sets *about* the
|
|
14
20
|
* document rather than as body content. Rendered via the detail editor's own
|
|
15
21
|
* field-row renderer, so they behave identically to a content field.
|
|
16
22
|
*
|
|
17
|
-
*
|
|
18
|
-
* tabs. The empty state shows only when there is
|
|
23
|
+
* All three are identity/metadata, not content, so they live here — off the content
|
|
24
|
+
* tabs. The empty state shows only when there is nothing at all to display.
|
|
19
25
|
*/
|
|
20
|
-
export function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, }) {
|
|
26
|
+
export function DetailsPane({ collection, doc, siblings, readOnly, fields, renderField, onAliasChange, urlContext, }) {
|
|
21
27
|
const slots = declaredAliases(collection);
|
|
22
28
|
const hasSlots = slots.length > 0;
|
|
23
29
|
const hasFields = fields.length > 0;
|
|
24
|
-
|
|
30
|
+
const routable = urlContext !== undefined && urlContext.routes.some((r) => r.document?.collection === collection.id);
|
|
31
|
+
return (_jsxs(Stack, { gap: "4", className: "p-3", children: [routable && urlContext ? (_jsx(DocumentUrlRow, { collection: collection, doc: doc, siblings: siblings, context: urlContext })) : null, routable && (hasSlots || hasFields) ? _jsx(Separator, {}) : null, hasSlots ? (_jsxs(Stack, { gap: "1", children: [_jsx(Text, { size: "xs", weight: "semibold", children: translate('cms.details.slot') }), _jsxs(Select, { value: doc.alias ?? '', disabled: readOnly, "aria-label": translate('cms.details.slot'), "data-testid": "alias-select", onChange: (e) => onAliasChange(e.target.value === '' ? undefined : e.target.value), children: [_jsx("option", { value: "", children: translate('cms.details.noSlot') }), slots.map((slot) => {
|
|
25
32
|
const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot);
|
|
26
33
|
const blocked = takenByOther && doc.alias !== slot;
|
|
27
34
|
return (_jsxs("option", { value: slot, disabled: blocked, children: [slot, blocked ? translate('cms.details.slotInUse') : ''] }, slot));
|
|
28
|
-
})] }), _jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.slotHint') })] })) : null, hasSlots && hasFields ? _jsx(Separator, {}) : null, hasFields ? _jsx(Stack, { gap: "4", children: fields.map(renderField) }) : null, !hasSlots && !hasFields ? (_jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.empty') })) : null] }));
|
|
35
|
+
})] }), _jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.slotHint') })] })) : null, hasSlots && hasFields ? _jsx(Separator, {}) : null, hasFields ? _jsx(Stack, { gap: "4", children: fields.map(renderField) }) : null, !routable && !hasSlots && !hasFields ? (_jsx(Text, { size: "xs", tone: "muted", children: translate('cms.details.empty') })) : null] }));
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The document-level URL row (0.0.39 — EC-315 "hybrid split"). The full composed path,
|
|
39
|
+
* classified segment by segment: an ancestor-DOCUMENT segment is a link to that
|
|
40
|
+
* document's editor (autosave keeps the current edit safe on navigate); a static
|
|
41
|
+
* route segment (mount prefix, or `blog` in `/blog/:slug`) is plain text; this
|
|
42
|
+
* document's own slug segment is highlighted as the current page. Draft-derived (it
|
|
43
|
+
* updates as you type) — the header's published-gated "View live" stays the sole
|
|
44
|
+
* open-live action, so the draft/live distinction is preserved here too.
|
|
45
|
+
*/
|
|
46
|
+
function DocumentUrlRow({ collection, doc, siblings, context, }) {
|
|
47
|
+
const segments = resolveUrlSegments({ routes: [...context.routes], localeConfig: context.localeConfig }, collection, doc, { siblings, locale: context.locale });
|
|
48
|
+
return (_jsxs(Stack, { gap: "1", "data-testid": "details-url", children: [_jsx(Text, { size: "xs", weight: "semibold", children: translate('cms.details.url') }), _jsx(Row, { gap: "none", align: "center", wrap: true, className: "font-mono text-xs text-muted-foreground", children: segments.length === 0 ? (_jsx("span", { "aria-hidden": true, children: "/" })) : (segments.map((seg, i) => (_jsxs("span", { className: "inline-flex items-center", children: [_jsx("span", { "aria-hidden": true, className: "text-muted-foreground/50", children: "/" }), seg.kind === 'ancestor' && seg.documentId ? (_jsx(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId: context.projectId, _splat: studioPath.cmsDocument(collection.id, seg.documentId) }, title: translate('cms.relation.openTitle', { title: seg.text }), "data-testid": "details-url-ancestor", className: "rounded px-0.5 text-primary hover:bg-primary/10 hover:underline", children: seg.text })) : (_jsx("span", { className: cn('px-0.5', seg.kind === 'current' && 'font-medium text-foreground'), title: seg.kind === 'current' ? translate('cms.details.urlCurrent') : undefined, children: seg.text }))] }, `${seg.text}-${i}`)))) })] }));
|
|
29
49
|
}
|
|
30
50
|
//# sourceMappingURL=details-pane.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"details-pane.js","sourceRoot":"","sources":["../../../src/components/cms/details-pane.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAA;AAG1D,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,4BAA4B,CAAA;AAEtD;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,WAAW,CAAC,EAC1B,UAAU,EACV,GAAG,EACH,QAAQ,EACR,QAAQ,EACR,MAAM,EACN,WAAW,EACX,aAAa,GAYd;IACC,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;IACzC,MAAM,QAAQ,GAAG,KAAK,CAAC,MAAM,GAAG,CAAC,CAAA;IACjC,MAAM,SAAS,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAA;IAEnC,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,aAC3B,QAAQ,CAAC,CAAC,CAAC,CACV,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,aACZ,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,MAAM,EAAC,UAAU,YAC9B,SAAS,CAAC,kBAAkB,CAAC,GACzB,EACP,MAAC,MAAM,IACL,KAAK,EAAE,GAAG,CAAC,KAAK,IAAI,EAAE,EACtB,QAAQ,EAAE,QAAQ,gBACN,SAAS,CAAC,kBAAkB,CAAC,iBAC7B,cAAc,EAC1B,QAAQ,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,KAAK,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,aAElF,iBAAQ,KAAK,EAAC,EAAE,YAAE,SAAS,CAAC,oBAAoB,CAAC,GAAU,EAC1D,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE;gCAClB,MAAM,YAAY,GAAG,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAA;gCAC9E,MAAM,OAAO,GAAG,YAAY,IAAI,GAAG,CAAC,KAAK,KAAK,IAAI,CAAA;gCAClD,OAAO,CACL,kBAAmB,KAAK,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,aAC9C,IAAI,EACJ,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,EAAE,KAFvC,IAAI,CAGR,CACV,CAAA;4BACH,CAAC,CAAC,IACK,EACT,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,sBAAsB,CAAC,GAC7B,IACD,CACT,CAAC,CAAC,CAAC,IAAI,EAEP,QAAQ,IAAI,SAAS,CAAC,CAAC,CAAC,KAAC,SAAS,KAAG,CAAC,CAAC,CAAC,IAAI,EAE5C,SAAS,CAAC,CAAC,CAAC,KAAC,KAAK,IAAC,GAAG,EAAC,GAAG,YAAE,MAAM,CAAC,GAAG,CAAC,WAAW,CAAC,GAAS,CAAC,CAAC,CAAC,IAAI,EAEnE,CAAC,QAAQ,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CACzB,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,SAAS,CAAC,mBAAmB,CAAC,GAC1B,CACR,CAAC,CAAC,CAAC,IAAI,IACF,CACT,CAAA;AACH,CAAC","sourcesContent":["import { declaredAliases } from '@elytracms/core/cms-core'\nimport type { CmsDocument, CollectionDef, FieldDef } from '@elytracms/core/cms-core'\nimport type { ReactNode } from 'react'\nimport { Select, Separator, Stack, Text } from '@elytracms/studio/ui'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/**\n * EC-284 + EC-285 — document-level metadata pane (the right inspector). Two\n * optional regions:\n *\n * 1. **Slot** (EC-284): a stable, named handle the site uses to find this document\n * (the main menu, the footer menu…) instead of which exact entry it is — shown\n * only when the collection declares alias slots.\n * 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,\n * author, publish date…) — document-level properties an editor sets *about* the\n * document rather than as body content. Rendered via the detail editor's own\n * field-row renderer, so they behave identically to a content field.\n *\n * Both are identity/metadata, not content, so they live here — off the content\n * tabs. The empty state shows only when there is neither a slot nor a sidebar field.\n */\nexport function DetailsPane({\n collection,\n doc,\n siblings,\n readOnly,\n fields,\n renderField,\n onAliasChange,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n /** The other documents in this collection (to show which slots are taken). */\n siblings: readonly CmsDocument[]\n readOnly?: boolean\n /** The collection's `sidebar`-group fields, in order (EC-285). */\n fields: readonly FieldDef[]\n /** Render one field row — shared with the content groups so behaviour matches. */\n renderField: (field: FieldDef) => ReactNode\n onAliasChange: (alias: string | undefined) => void\n}) {\n const slots = declaredAliases(collection)\n const hasSlots = slots.length > 0\n const hasFields = fields.length > 0\n\n return (\n <Stack gap=\"4\" className=\"p-3\">\n {hasSlots ? (\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.slot')}\n </Text>\n <Select\n value={doc.alias ?? ''}\n disabled={readOnly}\n aria-label={translate('cms.details.slot')}\n data-testid=\"alias-select\"\n onChange={(e) => onAliasChange(e.target.value === '' ? undefined : e.target.value)}\n >\n <option value=\"\">{translate('cms.details.noSlot')}</option>\n {slots.map((slot) => {\n const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot)\n const blocked = takenByOther && doc.alias !== slot\n return (\n <option key={slot} value={slot} disabled={blocked}>\n {slot}\n {blocked ? translate('cms.details.slotInUse') : ''}\n </option>\n )\n })}\n </Select>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.slotHint')}\n </Text>\n </Stack>\n ) : null}\n\n {hasSlots && hasFields ? <Separator /> : null}\n\n {hasFields ? <Stack gap=\"4\">{fields.map(renderField)}</Stack> : null}\n\n {!hasSlots && !hasFields ? (\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.empty')}\n </Text>\n ) : null}\n </Stack>\n )\n}\n"]}
|
|
1
|
+
{"version":3,"file":"details-pane.js","sourceRoot":"","sources":["../../../src/components/cms/details-pane.tsx"],"names":[],"mappings":"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","sourcesContent":["import { declaredAliases, resolveUrlSegments } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n FieldDef,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { ReactNode } from 'react'\nimport { Link, STUDIO_PROJECT_ROUTE, studioPath } from '@elytracms/studio/router'\nimport { Row, Select, Separator, Stack, Text } from '@elytracms/studio/ui'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { translate } from '@elytracms/studio/lib/i18n'\n\n/** 0.0.39 — routing context the Details pane needs to compose the document URL row. */\nexport interface DetailsUrlContext {\n projectId: string\n routes: readonly RouteRecord[]\n localeConfig: LocaleConfig\n locale: string\n}\n\n/**\n * EC-284 + EC-285 (+ 0.0.39 URL row) — document-level metadata pane (the right\n * inspector). Up to three optional regions:\n *\n * 1. **URL** (0.0.39): the full composed path as its own row — the EC-315 \"hybrid\n * split\" moves the composed URL out of the cramped slug field and up to the\n * document, where a URL is a first-class property. Ancestor-DOCUMENT segments\n * link to their editors; static route segments are plain text.\n * 2. **Slot** (EC-284): a stable, named handle the site uses to find this document\n * (the main menu, the footer menu…) instead of which exact entry it is — shown\n * only when the collection declares alias slots.\n * 3. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,\n * author, publish date…) — document-level properties an editor sets *about* the\n * document rather than as body content. Rendered via the detail editor's own\n * field-row renderer, so they behave identically to a content field.\n *\n * All three are identity/metadata, not content, so they live here — off the content\n * tabs. The empty state shows only when there is nothing at all to display.\n */\nexport function DetailsPane({\n collection,\n doc,\n siblings,\n readOnly,\n fields,\n renderField,\n onAliasChange,\n urlContext,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n /** The other documents in this collection (to show which slots are taken). */\n siblings: readonly CmsDocument[]\n readOnly?: boolean\n /** The collection's `sidebar`-group fields, in order (EC-285). */\n fields: readonly FieldDef[]\n /** Render one field row — shared with the content groups so behaviour matches. */\n renderField: (field: FieldDef) => ReactNode\n onAliasChange: (alias: string | undefined) => void\n /** Routing context for the composed URL row (0.0.39); omitted for non-routable rails. */\n urlContext?: DetailsUrlContext\n}) {\n const slots = declaredAliases(collection)\n const hasSlots = slots.length > 0\n const hasFields = fields.length > 0\n const routable =\n urlContext !== undefined && urlContext.routes.some((r) => r.document?.collection === collection.id)\n\n return (\n <Stack gap=\"4\" className=\"p-3\">\n {routable && urlContext ? (\n <DocumentUrlRow collection={collection} doc={doc} siblings={siblings} context={urlContext} />\n ) : null}\n\n {routable && (hasSlots || hasFields) ? <Separator /> : null}\n\n {hasSlots ? (\n <Stack gap=\"1\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.slot')}\n </Text>\n <Select\n value={doc.alias ?? ''}\n disabled={readOnly}\n aria-label={translate('cms.details.slot')}\n data-testid=\"alias-select\"\n onChange={(e) => onAliasChange(e.target.value === '' ? undefined : e.target.value)}\n >\n <option value=\"\">{translate('cms.details.noSlot')}</option>\n {slots.map((slot) => {\n const takenByOther = siblings.some((s) => s.id !== doc.id && s.alias === slot)\n const blocked = takenByOther && doc.alias !== slot\n return (\n <option key={slot} value={slot} disabled={blocked}>\n {slot}\n {blocked ? translate('cms.details.slotInUse') : ''}\n </option>\n )\n })}\n </Select>\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.slotHint')}\n </Text>\n </Stack>\n ) : null}\n\n {hasSlots && hasFields ? <Separator /> : null}\n\n {hasFields ? <Stack gap=\"4\">{fields.map(renderField)}</Stack> : null}\n\n {!routable && !hasSlots && !hasFields ? (\n <Text size=\"xs\" tone=\"muted\">\n {translate('cms.details.empty')}\n </Text>\n ) : null}\n </Stack>\n )\n}\n\n/**\n * The document-level URL row (0.0.39 — EC-315 \"hybrid split\"). The full composed path,\n * classified segment by segment: an ancestor-DOCUMENT segment is a link to that\n * document's editor (autosave keeps the current edit safe on navigate); a static\n * route segment (mount prefix, or `blog` in `/blog/:slug`) is plain text; this\n * document's own slug segment is highlighted as the current page. Draft-derived (it\n * updates as you type) — the header's published-gated \"View live\" stays the sole\n * open-live action, so the draft/live distinction is preserved here too.\n */\nfunction DocumentUrlRow({\n collection,\n doc,\n siblings,\n context,\n}: {\n collection: CollectionDef\n doc: CmsDocument\n siblings: readonly CmsDocument[]\n context: DetailsUrlContext\n}) {\n const segments = resolveUrlSegments(\n { routes: [...context.routes], localeConfig: context.localeConfig },\n collection,\n doc,\n { siblings, locale: context.locale },\n )\n return (\n <Stack gap=\"1\" data-testid=\"details-url\">\n <Text size=\"xs\" weight=\"semibold\">\n {translate('cms.details.url')}\n </Text>\n <Row gap=\"none\" align=\"center\" wrap className=\"font-mono text-xs text-muted-foreground\">\n {segments.length === 0 ? (\n <span aria-hidden>/</span>\n ) : (\n segments.map((seg, i) => (\n <span key={`${seg.text}-${i}`} className=\"inline-flex items-center\">\n <span aria-hidden className=\"text-muted-foreground/50\">\n /\n </span>\n {seg.kind === 'ancestor' && seg.documentId ? (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId: context.projectId, _splat: studioPath.cmsDocument(collection.id, seg.documentId) }}\n title={translate('cms.relation.openTitle', { title: seg.text })}\n data-testid=\"details-url-ancestor\"\n className=\"rounded px-0.5 text-primary hover:bg-primary/10 hover:underline\"\n >\n {seg.text}\n </Link>\n ) : (\n <span\n className={cn('px-0.5', seg.kind === 'current' && 'font-medium text-foreground')}\n title={seg.kind === 'current' ? translate('cms.details.urlCurrent') : undefined}\n >\n {seg.text}\n </span>\n )}\n </span>\n ))\n )}\n </Row>\n </Stack>\n )\n}\n"]}
|
|
@@ -150,8 +150,13 @@ function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
|
150
150
|
const current = typeof value === 'string' ? value : '';
|
|
151
151
|
const locale = source.activeLocale;
|
|
152
152
|
const localeConfig = source.localeConfig;
|
|
153
|
-
|
|
154
|
-
|
|
153
|
+
// 0.0.39 regression fix: resolve the deployment routes on EVERY render (a cheap
|
|
154
|
+
// synchronous accessor), never freeze them at first paint. The earlier
|
|
155
|
+
// `useMemo(() => resolveDeploymentRoutes(), [])` captured the EMPTY route set from
|
|
156
|
+
// before the workspace/config finished loading — so the composed path never
|
|
157
|
+
// appeared once routes arrived, and a stale set could paint a misleading prefix.
|
|
158
|
+
const routes = resolveDeploymentRoutes();
|
|
159
|
+
const routable = routes.some((r) => r.document?.collection === source.collectionId);
|
|
155
160
|
const draft = editor?.draft;
|
|
156
161
|
// The active-locale value bag the auto-slug source derives from (title, etc.).
|
|
157
162
|
const values = useMemo(() => (draft ? activeLocaleValues(draft, locale) : {}), [draft, locale]);
|
|
@@ -174,11 +179,9 @@ function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
|
174
179
|
// read-only preview that updates as you type, mirroring the detail editor's draft
|
|
175
180
|
// URL. The header's published-gated "View live" stays the sole open-live action,
|
|
176
181
|
// so the draft/live distinction is preserved (an unpublished slug never links out).
|
|
177
|
-
const parts =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return resolveSlugPath({ routes, localeConfig }, source.schema, draft, { siblings, locale });
|
|
181
|
-
}, [draft, routable, routes, localeConfig, source.schema, locale]);
|
|
182
|
+
const parts = draft && routable
|
|
183
|
+
? resolveSlugPath({ routes, localeConfig }, source.schema, draft, { siblings, locale })
|
|
184
|
+
: undefined;
|
|
182
185
|
// Default (sync) uniqueness — the first colliding sibling's title, or null.
|
|
183
186
|
const defaultCollisionTitle = useMemo(() => {
|
|
184
187
|
if (slugConfig?.unique || !draft)
|
|
@@ -244,11 +247,17 @@ function SlugField({ field, value, onChange, readOnly, id, }) {
|
|
|
244
247
|
}
|
|
245
248
|
const input = (_jsx(Input, { id: id, disabled: readOnly, value: current, placeholder: derived || undefined, onChange: (e) => handleInput(e.target.value), className: cn(parts ? 'h-7 min-w-[10ch] flex-1 bg-background font-mono text-xs' : 'flex-1') }));
|
|
246
249
|
const regenerateButton = !readOnly ? (_jsx(Button, { type: "button", variant: "ghost", size: "icon-sm", onClick: regenerate, "aria-label": translate('cms.field.slug.regenerate'), title: translate('cms.field.slug.regenerate'), children: _jsx(RotateCw, {}) })) : null;
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
// The compact, ellipsized read-only prefix (0.0.39 "hybrid split"): only the LAST
|
|
251
|
+
// context segment (`…/produkte/`), so a deep path can't overflow the narrow Details
|
|
252
|
+
// sidebar. The full composed path is the tooltip AND the document-level URL row in
|
|
253
|
+
// the Details pane (which owns the composed URL — a first-class document property).
|
|
254
|
+
const prefixSegments = parts
|
|
255
|
+
? [...(parts.localePrefix ? [parts.localePrefix] : []), ...parts.parentSegments]
|
|
256
|
+
: [];
|
|
257
|
+
const lastSegment = prefixSegments[prefixSegments.length - 1];
|
|
258
|
+
const compactPrefix = lastSegment ? `${prefixSegments.length > 1 ? '…/' : '/'}${lastSegment}/` : '/';
|
|
259
|
+
const prefixTitle = parts?.path ?? (prefixSegments.length > 0 ? `/${prefixSegments.join('/')}/` : '/');
|
|
260
|
+
return (_jsxs(Stack, { gap: "2", children: [parts ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-composed-path", children: [_jsx("span", { className: "shrink-0 rounded bg-muted/40 px-1.5 py-1 font-mono text-xs text-muted-foreground ring-1 ring-inset ring-border", title: prefixTitle, "data-testid": "slug-prefix", children: compactPrefix }), input, regenerateButton] })) : (_jsxs(Row, { gap: "1", align: "center", children: [input, regenerateButton] })), collisionMessage ? (_jsxs(Row, { gap: "1", align: "center", "data-testid": "slug-collision", children: [_jsx(AlertTriangle, { className: "size-3.5 shrink-0 text-warning", "aria-hidden": true }), _jsx(Text, { size: "xs", tone: "muted", children: collisionMessage })] })) : null] }));
|
|
252
261
|
}
|
|
253
262
|
function asRecord(value) {
|
|
254
263
|
return value !== null && typeof value === 'object' && !Array.isArray(value)
|