@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.
Files changed (49) hide show
  1. package/dist/components/cms/collection-table.js +13 -1
  2. package/dist/components/cms/collection-table.js.map +1 -1
  3. package/dist/components/cms/detail-editor.js +116 -72
  4. package/dist/components/cms/detail-editor.js.map +1 -1
  5. package/dist/components/cms/details-pane.d.ts +21 -8
  6. package/dist/components/cms/details-pane.js +31 -11
  7. package/dist/components/cms/details-pane.js.map +1 -1
  8. package/dist/components/cms/field-control.js +21 -12
  9. package/dist/components/cms/field-control.js.map +1 -1
  10. package/dist/components/cms/prop-fields.js +25 -1
  11. package/dist/components/cms/prop-fields.js.map +1 -1
  12. package/dist/components/cms/rich-text/component-embed.d.ts +7 -2
  13. package/dist/components/cms/rich-text/component-embed.js +60 -5
  14. package/dist/components/cms/rich-text/component-embed.js.map +1 -1
  15. package/dist/components/cms/rich-text/editor.js +24 -5
  16. package/dist/components/cms/rich-text/editor.js.map +1 -1
  17. package/dist/components/cms/rich-text/embed-inspector-context.d.ts +50 -0
  18. package/dist/components/cms/rich-text/embed-inspector-context.js +12 -0
  19. package/dist/components/cms/rich-text/embed-inspector-context.js.map +1 -0
  20. package/dist/components/cms/rich-text/embed-inspector.d.ts +25 -0
  21. package/dist/components/cms/rich-text/embed-inspector.js +48 -0
  22. package/dist/components/cms/rich-text/embed-inspector.js.map +1 -0
  23. package/dist/components/cms/rich-text/embed-picker.d.ts +0 -2
  24. package/dist/components/cms/rich-text/embed-picker.js +0 -4
  25. package/dist/components/cms/rich-text/embed-picker.js.map +1 -1
  26. package/dist/components/cms/rich-text/embed-props.d.ts +7 -0
  27. package/dist/components/cms/rich-text/embed-props.js +9 -0
  28. package/dist/components/cms/rich-text/embed-props.js.map +1 -1
  29. package/dist/components/cms/rich-text/insert.d.ts +8 -2
  30. package/dist/components/cms/rich-text/insert.js +32 -3
  31. package/dist/components/cms/rich-text/insert.js.map +1 -1
  32. package/dist/components/cms/rich-text/link-control.js +3 -1
  33. package/dist/components/cms/rich-text/link-control.js.map +1 -1
  34. package/dist/components/cms/rich-text/slash-command.d.ts +23 -6
  35. package/dist/components/cms/rich-text/slash-command.js +31 -23
  36. package/dist/components/cms/rich-text/slash-command.js.map +1 -1
  37. package/dist/components/cms/rich-text/toolbar-state.d.ts +45 -0
  38. package/dist/components/cms/rich-text/toolbar-state.js +66 -0
  39. package/dist/components/cms/rich-text/toolbar-state.js.map +1 -0
  40. package/dist/components/cms/rich-text/toolbar.js +7 -15
  41. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  42. package/dist/components/cms/version-compare.js +51 -22
  43. package/dist/components/cms/version-compare.js.map +1 -1
  44. package/dist/components/components/component-preview.d.ts +36 -0
  45. package/dist/components/components/component-preview.js +157 -5
  46. package/dist/components/components/component-preview.js.map +1 -1
  47. package/dist/lib/i18n/messages.en.js +18 -3
  48. package/dist/lib/i18n/messages.en.js.map +1 -1
  49. 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 inspector). Two
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. **Slot** (EC-284): a stable, named handle the site uses to find this document
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
- * 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
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
- * Both are identity/metadata, not content, so they live here — off the content
16
- * tabs. The empty state shows only when there is neither a slot nor a sidebar field.
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 { Select, Separator, Stack, Text } from '@elytracms/studio/ui';
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 inspector). Two
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. **Slot** (EC-284): a stable, named handle the site uses to find this document
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
- * 2. **Sidebar fields** (EC-285): the document's `sidebar`-group fields (slug,
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
- * Both are identity/metadata, not content, so they live here — off the content
18
- * tabs. The empty state shows only when there is neither a slot nor a sidebar field.
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
- return (_jsxs(Stack, { gap: "4", className: "p-3", children: [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) => {
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
- const routes = useMemo(() => resolveDeploymentRoutes(), []);
154
- const routable = useMemo(() => routes.some((r) => r.document?.collection === source.collectionId), [routes, source.collectionId]);
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 = useMemo(() => {
178
- if (!draft || !routable)
179
- return undefined;
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
- return (_jsxs(Stack, { gap: "2", children: [parts ? (_jsxs(Row, { gap: "1", align: "center", wrap: true, className: "rounded-md border border-input bg-muted/30 px-2 py-1 font-mono text-xs text-muted-foreground", "data-testid": "slug-composed-path", children: [parts.localePrefix ? _jsx(SlugSegment, { children: parts.localePrefix }) : null, _jsx("span", { "aria-hidden": true, children: "/" }), parts.parentSegments.map((seg, i) => (_jsxs("span", { className: "inline-flex items-center gap-1", children: [_jsx(SlugSegment, { children: seg }), _jsx("span", { "aria-hidden": true, children: "/" })] }, `${seg}-${i}`))), input, regenerateButton] })) : (_jsxs(Row, { gap: "1", align: "center", children: [input, regenerateButton] })), routable ? (_jsxs(Row, { gap: "1", align: "center", className: "text-xs text-muted-foreground", "data-testid": "slug-path-preview", children: [_jsx(Link2, { className: "size-3 shrink-0", "aria-hidden": true }), parts?.path ? (_jsx(Text, { as: "span", size: "xs", tone: "muted", className: "min-w-0 truncate", title: parts.path, children: translate('cms.editor.servedAt', { path: parts.path }) })) : (_jsx(Text, { as: "span", size: "xs", tone: "muted", children: translate('cms.editor.slugUrlHint') }))] })) : null, 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] }));
248
- }
249
- /** A single read-only path segment, subtly boxed to read as fixed context (EC-315). */
250
- function SlugSegment({ children }) {
251
- return (_jsx("span", { className: "rounded bg-background px-1.5 py-0.5 text-foreground/70 ring-1 ring-inset ring-border", children: children }));
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)