@elytracms/studio 0.0.59 → 0.0.61

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 (38) hide show
  1. package/dist/components/ai/proposal-card.js +14 -23
  2. package/dist/components/ai/proposal-card.js.map +1 -1
  3. package/dist/components/app-views/studio-config.d.ts +34 -0
  4. package/dist/components/app-views/studio-config.js.map +1 -1
  5. package/dist/components/builder/canvas-chrome.d.ts +65 -0
  6. package/dist/components/builder/canvas-chrome.js +125 -0
  7. package/dist/components/builder/canvas-chrome.js.map +1 -0
  8. package/dist/components/builder/canvas.js +13 -91
  9. package/dist/components/builder/canvas.js.map +1 -1
  10. package/dist/components/cms/prop-fields.d.ts +15 -0
  11. package/dist/components/cms/prop-fields.js +38 -1
  12. package/dist/components/cms/prop-fields.js.map +1 -1
  13. package/dist/components/cms/rich-text/editor.d.ts +8 -0
  14. package/dist/components/cms/rich-text/editor.js +38 -3
  15. package/dist/components/cms/rich-text/editor.js.map +1 -1
  16. package/dist/components/cms/rich-text/toolbar.d.ts +16 -1
  17. package/dist/components/cms/rich-text/toolbar.js +15 -4
  18. package/dist/components/cms/rich-text/toolbar.js.map +1 -1
  19. package/dist/components/project-session.d.ts +31 -1
  20. package/dist/components/project-session.js +56 -22
  21. package/dist/components/project-session.js.map +1 -1
  22. package/dist/customization.d.ts +1 -1
  23. package/dist/customization.js.map +1 -1
  24. package/dist/lib/i18n/messages.en.js +5 -0
  25. package/dist/lib/i18n/messages.en.js.map +1 -1
  26. package/dist/lib/workspace/index.d.ts +8 -0
  27. package/dist/lib/workspace/index.js +10 -0
  28. package/dist/lib/workspace/index.js.map +1 -1
  29. package/dist/ui/expandable-surface.d.ts +48 -0
  30. package/dist/ui/expandable-surface.js +72 -0
  31. package/dist/ui/expandable-surface.js.map +1 -0
  32. package/dist/ui/index.d.ts +2 -0
  33. package/dist/ui/index.js +2 -0
  34. package/dist/ui/index.js.map +1 -1
  35. package/dist/ui/layer-stack.d.ts +36 -0
  36. package/dist/ui/layer-stack.js +42 -0
  37. package/dist/ui/layer-stack.js.map +1 -0
  38. package/package.json +2 -2
@@ -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 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\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}\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({ editor, targets, onOpenImage, onOpenEmbed }: 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=\"h-10 flex-wrap gap-0.5 rounded-none border-x-0 border-t-0 bg-muted/30 px-1.5\">\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 </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 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,5 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations';
2
+ import type { LocaleConfig, RouteRecord } from '@elytracms/core/cms-core';
3
+ import type { HydratedCms, HydratedProjectSession, OperationResult, OperationsClient } from '@elytracms/studio/lib/operations';
4
+ import type { ContentSessionState } from '@elytracms/studio/lib/data';
3
5
  /**
4
6
  * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).
5
7
  *
@@ -56,6 +58,34 @@ export interface ProjectSessionStore {
56
58
  loadCms(): Promise<OperationResult<HydratedCms | null>>;
57
59
  }
58
60
  declare const useProjectSession: () => ProjectSessionStore;
61
+ /**
62
+ * Compose the preview content-session state from a hydrated CMS load plus the
63
+ * config-owned deployment seams — the ONE place hydrated content meets config:
64
+ *
65
+ * - `routes` (EC-291): the config-sourced routes, so the canvas `resolveLink`
66
+ * derives a rich-text link's URL exactly as delivery does.
67
+ * - `localeConfig` (EC-385): the active config project's declared locales
68
+ * (AD-11 — locales are structure, structure is code), so canvas content
69
+ * clients evaluate locale-shaped values (EC-018 fallback, localized
70
+ * filter/sort) over the PROJECT's locale set, never the sample `en/de`
71
+ * fixture. Without a config project (env-only wiring) the key stays absent
72
+ * and the preview engine keeps its sample-config default, mirroring the CMS
73
+ * surface ladder (`resolveCmsInitialLocaleConfig`).
74
+ *
75
+ * The parameters exist purely as a unit-test seam (house pattern, same as
76
+ * `resolveCmsInitialCollections`); app code always uses the defaults.
77
+ */
78
+ export declare function contentSessionOf(cms: HydratedCms | null, routes?: readonly RouteRecord[], localeConfig?: LocaleConfig | undefined): ContentSessionState | null;
79
+ /**
80
+ * Load the CURRENT persisted CMS state and point the preview content engine at
81
+ * it — the framework-free core of `loadCms` (both modes run it; EC-361). The
82
+ * playground return contract (EC-320) is pinned here: the GATE value is always
83
+ * `{ ok: true, value: null }` in playground — CMS surfaces keep their
84
+ * config/fixture baseline — while the content session above still hydrates
85
+ * from the in-memory adapter (a load failure is loud, never a silent empty
86
+ * preview).
87
+ */
88
+ export declare function loadCmsIntoSession(operations: OperationsClient, projectId: string, playground: boolean): Promise<OperationResult<HydratedCms | null>>;
59
89
  export declare function ProjectSessionProvider({ projectId, children, }: {
60
90
  projectId: string;
61
91
  children: ReactNode;
@@ -3,21 +3,64 @@ import { useCallback, useEffect } from 'react';
3
3
  import { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations';
4
4
  import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
5
5
  import { configureContentSession } from '@elytracms/studio/lib/data';
6
- import { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
6
+ import { resolveDeploymentLocaleConfig, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace';
7
7
  import { createRequiredContext } from '@elytracms/studio/lib/required-context';
8
8
  import { HydrationBoundary, useHydrationPhase } from './use-hydration-phase.js';
9
9
  const { Context: ProjectSessionContext, useRequired: useProjectSession } = createRequiredContext('ProjectSession');
10
+ /**
11
+ * Compose the preview content-session state from a hydrated CMS load plus the
12
+ * config-owned deployment seams — the ONE place hydrated content meets config:
13
+ *
14
+ * - `routes` (EC-291): the config-sourced routes, so the canvas `resolveLink`
15
+ * derives a rich-text link's URL exactly as delivery does.
16
+ * - `localeConfig` (EC-385): the active config project's declared locales
17
+ * (AD-11 — locales are structure, structure is code), so canvas content
18
+ * clients evaluate locale-shaped values (EC-018 fallback, localized
19
+ * filter/sort) over the PROJECT's locale set, never the sample `en/de`
20
+ * fixture. Without a config project (env-only wiring) the key stays absent
21
+ * and the preview engine keeps its sample-config default, mirroring the CMS
22
+ * surface ladder (`resolveCmsInitialLocaleConfig`).
23
+ *
24
+ * The parameters exist purely as a unit-test seam (house pattern, same as
25
+ * `resolveCmsInitialCollections`); app code always uses the defaults.
26
+ */
27
+ export function contentSessionOf(cms, routes = resolveDeploymentRoutes(), localeConfig = resolveDeploymentLocaleConfig()) {
28
+ if (!cms)
29
+ return null;
30
+ return { ...cms, routes, ...(localeConfig ? { localeConfig } : {}) };
31
+ }
32
+ /**
33
+ * Load the CURRENT persisted CMS state and point the preview content engine at
34
+ * it — the framework-free core of `loadCms` (both modes run it; EC-361). The
35
+ * playground return contract (EC-320) is pinned here: the GATE value is always
36
+ * `{ ok: true, value: null }` in playground — CMS surfaces keep their
37
+ * config/fixture baseline — while the content session above still hydrates
38
+ * from the in-memory adapter (a load failure is loud, never a silent empty
39
+ * preview).
40
+ */
41
+ export async function loadCmsIntoSession(operations, projectId, playground) {
42
+ const result = await loadHydratedCms(operations, projectId);
43
+ // Keep the preview engine in sync with the freshest persisted content.
44
+ if (result.ok && result.value !== null)
45
+ configureContentSession(contentSessionOf(result.value));
46
+ if (playground) {
47
+ if (!result.ok)
48
+ console.error('[elytra] playground content-session load failed', result.error);
49
+ return { ok: true, value: null };
50
+ }
51
+ return result;
52
+ }
10
53
  export function ProjectSessionProvider({ projectId, children, }) {
11
54
  const playground = isPlaygroundMode();
12
55
  const loader = useCallback(() => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)), [projectId]);
13
56
  // Point the preview content engine at the hydrated documents BEFORE any
14
- // child renders (panels read it module-level, synchronously). EC-291:
15
- // include the config-sourced routes so the canvas `resolveLink` derives a
16
- // rich-text link's URL exactly as delivery does. `enabled: true` below runs
17
- // this in the playground fast-path too (with `session: null`) — the one
18
- // divergence from `CmsSessionGate`, which owns no such side effect.
57
+ // child renders (panels read it module-level, synchronously), composed with
58
+ // the config-owned routes (EC-291) + locale config (EC-385) via
59
+ // `contentSessionOf`. `enabled: true` below runs this in the playground
60
+ // fast-path too (with `session: null`) — the one divergence from
61
+ // `CmsSessionGate`, which owns no such side effect.
19
62
  const onResolved = useCallback((session) => {
20
- configureContentSession(session?.cms ? { ...session.cms, routes: resolveDeploymentRoutes() } : null);
63
+ configureContentSession(contentSessionOf(session?.cms ?? null));
21
64
  }, []);
22
65
  const { phase, retry } = useHydrationPhase(loader, {
23
66
  playground,
@@ -28,23 +71,14 @@ export function ProjectSessionProvider({ projectId, children, }) {
28
71
  // Leaving the project resets the preview engine to fixtures; the next
29
72
  // project's gate reconfigures it before rendering its own children.
30
73
  useEffect(() => () => configureContentSession(null), []);
74
+ // EC-361: the playground runs the SAME load (over the seeded in-memory
75
+ // adapter) for the content-session side effect, but the GATE value stays
76
+ // `null` — CMS surfaces keep their config/fixture baseline (EC-320's
77
+ // static-baseline rule), matching the pre-EC-361 playground contract. The
78
+ // pure core (`loadCmsIntoSession`, above) carries + pins that contract.
31
79
  const loadCms = useCallback(async () => {
32
80
  const { operations } = await getStudioOperations();
33
- const result = await loadHydratedCms(operations, projectId);
34
- // Keep the preview engine in sync with the freshest persisted content.
35
- if (result.ok && result.value !== null)
36
- configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() });
37
- // EC-361: the playground runs the SAME load (over the seeded in-memory
38
- // adapter) for the content-session side effect above, but the GATE value
39
- // stays `null` — CMS surfaces keep their config/fixture baseline (EC-320's
40
- // static-baseline rule), matching the pre-EC-361 playground contract. An
41
- // in-memory failure is loud, not a silent empty preview.
42
- if (playground) {
43
- if (!result.ok)
44
- console.error('[elytra] playground content-session load failed', result.error);
45
- return { ok: true, value: null };
46
- }
47
- return result;
81
+ return loadCmsIntoSession(operations, projectId, playground);
48
82
  }, [playground, projectId]);
49
83
  // EC-361: playground boot hydration for the content preview engine — the same
50
84
  // loader connected mode gates on, fired WITHOUT gating so the synchronous
@@ -1 +1 @@
1
- {"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type { HydratedCms, HydratedProjectSession, OperationResult } from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-361 — the playground gets the SAME side effect, without the render gate:\n * the seeded in-memory adapter IS a persisted backend behind the operations\n * seam (AD-17), so the content session hydrates from it through the identical\n * `loadHydratedCms` loader connected mode uses — fire-and-forget after the\n * synchronous first render (EC-138 stays intact), refreshed per CMS surface\n * mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas\n * listing/relation population therefore resolves from in-memory session\n * documents with connected-mode semantics (published-ness = the version\n * pointer; the Draft view resolves live rows) instead of the blanket-empty\n * session the playground used to run with. The React `session`/`loadCms`\n * VALUES stay `null` in playground — the CMS surfaces keep their fixture\n * baseline (EC-320's static-baseline rule); only the preview content engine\n * reads persisted state.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously). EC-291:\n // include the config-sourced routes so the canvas `resolveLink` derives a\n // rich-text link's URL exactly as delivery does. `enabled: true` below runs\n // this in the playground fast-path too (with `session: null`) — the one\n // divergence from `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(\n session?.cms ? { ...session.cms, routes: resolveDeploymentRoutes() } : null,\n )\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n const { operations } = await getStudioOperations()\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null)\n configureContentSession({ ...result.value, routes: resolveDeploymentRoutes() })\n // EC-361: the playground runs the SAME load (over the seeded in-memory\n // adapter) for the content-session side effect above, but the GATE value\n // stays `null` — CMS surfaces keep their config/fixture baseline (EC-320's\n // static-baseline rule), matching the pre-EC-361 playground contract. An\n // in-memory failure is loud, not a silent empty preview.\n if (playground) {\n if (!result.ok) console.error('[elytra] playground content-session load failed', result.error)\n return { ok: true, value: null }\n }\n return result\n }, [playground, projectId])\n\n // EC-361: playground boot hydration for the content preview engine — the same\n // loader connected mode gates on, fired WITHOUT gating so the synchronous\n // playground first render (EC-138) is preserved. `loadCms` owns the\n // `configureContentSession` side effect; the resolved value is discarded here.\n // Runs after `useHydrationPhase`'s fast-path effect (registered above) has\n // reset the session, so the hydrated state always lands last.\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n *\n * EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),\n * but `loadCms` is still fired per mount for its content-session side effect —\n * the preview engine refreshes from the in-memory adapter on the same\n * per-CMS-mount rhythm connected mode gets from the gated load.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
1
+ {"version":3,"file":"project-session.js","sourceRoot":"","sources":["../../src/components/project-session.tsx"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useEffect } from 'react'\nimport type { ReactNode } from 'react'\nimport type { LocaleConfig, RouteRecord } from '@elytracms/core/cms-core'\nimport { getStudioOperations, loadHydratedCms, loadProjectSession } from '@elytracms/studio/lib/operations'\nimport type {\n HydratedCms,\n HydratedProjectSession,\n OperationResult,\n OperationsClient,\n} from '@elytracms/studio/lib/operations'\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport { configureContentSession } from '@elytracms/studio/lib/data'\nimport type { ContentSessionState } from '@elytracms/studio/lib/data'\nimport { resolveDeploymentLocaleConfig, resolveDeploymentRoutes } from '@elytracms/studio/lib/workspace'\nimport { createRequiredContext } from '@elytracms/studio/lib/required-context'\nimport { HydrationBoundary, useHydrationPhase } from './use-hydration-phase'\n\n/**\n * Project-session hydration gate (Milestone B sign-off fix, EC-177 gap 1).\n *\n * In Convex mode every project session must BOOT from persistence: the\n * builder from the latest persisted graph revision, the CMS from the saved\n * schema + documents, publishing from the persisted environment states.\n * This provider runs the `@elytracms/core/operations` hydration queries\n * (`lib/operations/session-hydration`) and — like `DeploymentSetupGate` — gates its\n * children on a deterministic loading state, so fixture content never\n * flashes (and is never editable) before the persisted state arrives.\n *\n * Playground mode renders children synchronously with `session: null`\n * (fixtures), byte-identical to the pre-hydration behavior — the EC-138\n * determinism contract.\n *\n * Side effect on hydration: the content preview engine\n * (`lib/data/collection-content`) is pointed at the hydrated documents\n * BEFORE children render, so \"Preview with\" lists and section previews show\n * live entries, not fixture titles.\n *\n * EC-361 — the playground gets the SAME side effect, without the render gate:\n * the seeded in-memory adapter IS a persisted backend behind the operations\n * seam (AD-17), so the content session hydrates from it through the identical\n * `loadHydratedCms` loader connected mode uses — fire-and-forget after the\n * synchronous first render (EC-138 stays intact), refreshed per CMS surface\n * mount exactly like connected's `CmsSessionGate` → `loadCms` rhythm. Canvas\n * listing/relation population therefore resolves from in-memory session\n * documents with connected-mode semantics (published-ness = the version\n * pointer; the Draft view resolves live rows) instead of the blanket-empty\n * session the playground used to run with. The React `session`/`loadCms`\n * VALUES stay `null` in playground — the CMS surfaces keep their fixture\n * baseline (EC-320's static-baseline rule); only the preview content engine\n * reads persisted state.\n *\n * EC-308/AD-17 note — this gate deliberately does NOT read through the\n * `useOperationQuery` cache seam. It is boot/session orchestration, not a\n * cacheable server read: (1) the synchronous playground first-render is the\n * EC-138 determinism contract (a pull-query is async even when disabled);\n * (2) `configureContentSession` must run synchronously BEFORE children mount,\n * which a post-render query effect cannot guarantee; (3) `CmsSessionGate` is\n * intentionally fresh-per-visit (it remounts to show edits persisted since\n * project open) — the opposite of a cached read. It still reads through the\n * operations client (no transport bypass), and uses a user-driven retry epoch,\n * not freshness machinery — so it is outside the patchwork AD-17 retires.\n */\n\nexport interface ProjectSessionStore {\n /** Hydrated persisted state (Convex mode); `null` in playground (fixtures). */\n session: HydratedProjectSession | null\n /**\n * Load the CURRENT persisted CMS state (fresh query — the CMS section\n * remounts per visit and must show edits persisted since project open).\n * Playground resolves `null` immediately (fixtures).\n */\n loadCms(): Promise<OperationResult<HydratedCms | null>>\n}\n\nconst { Context: ProjectSessionContext, useRequired: useProjectSession } =\n createRequiredContext<ProjectSessionStore>('ProjectSession')\n\n/**\n * Compose the preview content-session state from a hydrated CMS load plus the\n * config-owned deployment seams — the ONE place hydrated content meets config:\n *\n * - `routes` (EC-291): the config-sourced routes, so the canvas `resolveLink`\n * derives a rich-text link's URL exactly as delivery does.\n * - `localeConfig` (EC-385): the active config project's declared locales\n * (AD-11 — locales are structure, structure is code), so canvas content\n * clients evaluate locale-shaped values (EC-018 fallback, localized\n * filter/sort) over the PROJECT's locale set, never the sample `en/de`\n * fixture. Without a config project (env-only wiring) the key stays absent\n * and the preview engine keeps its sample-config default, mirroring the CMS\n * surface ladder (`resolveCmsInitialLocaleConfig`).\n *\n * The parameters exist purely as a unit-test seam (house pattern, same as\n * `resolveCmsInitialCollections`); app code always uses the defaults.\n */\nexport function contentSessionOf(\n cms: HydratedCms | null,\n routes: readonly RouteRecord[] = resolveDeploymentRoutes(),\n localeConfig: LocaleConfig | undefined = resolveDeploymentLocaleConfig(),\n): ContentSessionState | null {\n if (!cms) return null\n return { ...cms, routes, ...(localeConfig ? { localeConfig } : {}) }\n}\n\n/**\n * Load the CURRENT persisted CMS state and point the preview content engine at\n * it — the framework-free core of `loadCms` (both modes run it; EC-361). The\n * playground return contract (EC-320) is pinned here: the GATE value is always\n * `{ ok: true, value: null }` in playground — CMS surfaces keep their\n * config/fixture baseline — while the content session above still hydrates\n * from the in-memory adapter (a load failure is loud, never a silent empty\n * preview).\n */\nexport async function loadCmsIntoSession(\n operations: OperationsClient,\n projectId: string,\n playground: boolean,\n): Promise<OperationResult<HydratedCms | null>> {\n const result = await loadHydratedCms(operations, projectId)\n // Keep the preview engine in sync with the freshest persisted content.\n if (result.ok && result.value !== null) configureContentSession(contentSessionOf(result.value))\n if (playground) {\n if (!result.ok) console.error('[elytra] playground content-session load failed', result.error)\n return { ok: true, value: null }\n }\n return result\n}\n\nexport function ProjectSessionProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const playground = isPlaygroundMode()\n\n const loader = useCallback(\n () => getStudioOperations().then(({ operations }) => loadProjectSession(operations, projectId)),\n [projectId],\n )\n // Point the preview content engine at the hydrated documents BEFORE any\n // child renders (panels read it module-level, synchronously), composed with\n // the config-owned routes (EC-291) + locale config (EC-385) via\n // `contentSessionOf`. `enabled: true` below runs this in the playground\n // fast-path too (with `session: null`) — the one divergence from\n // `CmsSessionGate`, which owns no such side effect.\n const onResolved = useCallback((session: HydratedProjectSession | null) => {\n configureContentSession(contentSessionOf(session?.cms ?? null))\n }, [])\n const { phase, retry } = useHydrationPhase<HydratedProjectSession | null>(loader, {\n playground,\n playgroundValue: null,\n enabled: true,\n onResolved,\n })\n\n // Leaving the project resets the preview engine to fixtures; the next\n // project's gate reconfigures it before rendering its own children.\n useEffect(() => () => configureContentSession(null), [])\n\n // EC-361: the playground runs the SAME load (over the seeded in-memory\n // adapter) for the content-session side effect, but the GATE value stays\n // `null` — CMS surfaces keep their config/fixture baseline (EC-320's\n // static-baseline rule), matching the pre-EC-361 playground contract. The\n // pure core (`loadCmsIntoSession`, above) carries + pins that contract.\n const loadCms = useCallback(async (): Promise<OperationResult<HydratedCms | null>> => {\n const { operations } = await getStudioOperations()\n return loadCmsIntoSession(operations, projectId, playground)\n }, [playground, projectId])\n\n // EC-361: playground boot hydration for the content preview engine — the same\n // loader connected mode gates on, fired WITHOUT gating so the synchronous\n // playground first render (EC-138) is preserved. `loadCms` owns the\n // `configureContentSession` side effect; the resolved value is discarded here.\n // Runs after `useHydrationPhase`'s fast-path effect (registered above) has\n // reset the session, so the hydrated state always lands last.\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {(session) => (\n <ProjectSessionContext.Provider value={{ session, loadCms }}>\n {children}\n </ProjectSessionContext.Provider>\n )}\n </HydrationBoundary>\n )\n}\n\nexport { useProjectSession }\n\n/**\n * Gate one CMS surface mount on a FRESH persisted CMS load (Convex mode).\n * Render-prop: hands the hydrated content (or `null` → fixture fallback,\n * first-run) to the caller once it resolved. Playground renders synchronously\n * with `null`. Unlike `ProjectSessionProvider`, this gate owns no\n * `configureContentSession` side effect of its own — `loadCms` (from\n * `useProjectSession`) already calls it internally when it resolves — so no\n * `onResolved`/`enabled` is passed here.\n *\n * EC-361: in playground the gate stays synchronous (fixture baseline, EC-138),\n * but `loadCms` is still fired per mount for its content-session side effect —\n * the preview engine refreshes from the in-memory adapter on the same\n * per-CMS-mount rhythm connected mode gets from the gated load.\n */\nexport function CmsSessionGate({\n children,\n}: {\n children: (cms: HydratedCms | null) => ReactNode\n}) {\n const { loadCms } = useProjectSession()\n const playground = isPlaygroundMode()\n useEffect(() => {\n if (playground) void loadCms()\n }, [playground, loadCms])\n const { phase, retry } = useHydrationPhase<HydratedCms | null>(loadCms, {\n playground,\n playgroundValue: null,\n })\n return (\n <HydrationBoundary phase={phase} retry={retry}>\n {children}\n </HydrationBoundary>\n )\n}\n"]}
@@ -39,7 +39,7 @@
39
39
  * ```
40
40
  */
41
41
  export { defineStudio } from './components/app-views/studio-config.js';
42
- export type { StudioConfig, CustomFieldRenderer, CustomFieldRendererProps, } from './components/app-views/studio-config.js';
42
+ export type { StudioConfig, StudioCanvasTheme, CustomFieldRenderer, CustomFieldRendererProps, } from './components/app-views/studio-config.js';
43
43
  export { nav } from './components/app-views/default-modules.js';
44
44
  export type { CmsViewConfig, BuiltinViewConfig, CustomViewConfig, } from './components/app-views/default-modules.js';
45
45
  export type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module.js';
@@ -1 +1 @@
1
- {"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAMnE,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;GAIG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n *\n * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n/**\n * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({\n * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator\n * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.\n */\nexport { msg } from './lib/i18n'\nexport type { MessageCatalog, SourceMessage, StudioI18nConfig } from './lib/i18n'\n"]}
1
+ {"version":3,"file":"customization.js","sourceRoot":"","sources":["../src/customization.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AACH,OAAO,EAAE,YAAY,EAAE,MAAM,sCAAsC,CAAA;AAOnE,OAAO,EAAE,GAAG,EAAE,MAAM,wCAAwC,CAAA;AAO5D,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAC9C,oFAAoF;AACpF,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AACpE;;;;GAIG;AACH,OAAO,EAAE,GAAG,EAAE,MAAM,YAAY,CAAA","sourcesContent":["/**\n * `@elytracms/studio/customization` — the public customization API for a consuming\n * repo's `studio.config.tsx` (EC-236/237/238/239).\n *\n * Everything a repo customizes about the builder UI is **live studio code passed by\n * reference** through `defineStudio({...})` — the section rail (`nav.*`), custom\n * field renderers, and (in time) branding. This config is studio-only: it is handed\n * to `createStudio(config, studioConfig)` at the mount and never enters the shared\n * `elytra.config.ts` / the `frontend/` delivery bundle. The customization principle\n * in one import surface: direct values, no registries, no string ids — the lone\n * string seam is `fieldRenderers`' control key, which the delivery-shared schema\n * references via `form.control`.\n *\n * @example\n * ```tsx\n * // studio.config.tsx\n * import { defineStudio, nav } from '@elytracms/studio/customization'\n * import { ColorField } from './fields/color'\n *\n * export default defineStudio({\n * sidebar: [\n * nav.cms({ label: 'Content' }),\n * nav.group('Catalog', [\n * nav.cms({ label: 'Catalog', collections: ['product', 'productCategory'] }),\n * nav.cms({ label: 'Categories', collection: 'productCategory' }),\n * ]),\n * nav.separator(),\n * nav.assets(),\n * nav.settings(),\n * ],\n * fieldRenderers: { color: ColorField },\n * })\n *\n * // router.tsx (the mount)\n * import { createStudio } from '@elytracms/studio/customization'\n * import config from '../../elytra.config'\n * import studioConfig from './studio.config'\n * createStudio(config, studioConfig)\n * ```\n */\nexport { defineStudio } from './components/app-views/studio-config'\nexport type {\n StudioConfig,\n StudioCanvasTheme,\n CustomFieldRenderer,\n CustomFieldRendererProps,\n} from './components/app-views/studio-config'\nexport { nav } from './components/app-views/default-modules'\nexport type {\n CmsViewConfig,\n BuiltinViewConfig,\n CustomViewConfig,\n} from './components/app-views/default-modules'\nexport type { AppViewContext, AppViewModule, SidebarNode } from './components/app-views/module'\nexport { createStudio } from './lib/workspace'\n/** A ready-made example renderer; a consuming repo may use it or author its own. */\nexport { colorFieldRenderer } from './components/cms/field-renderer'\n/**\n * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({\n * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator\n * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.\n */\nexport { msg } from './lib/i18n'\nexport type { MessageCatalog, SourceMessage, StudioI18nConfig } from './lib/i18n'\n"]}
@@ -878,6 +878,11 @@ export const EN_MESSAGES = {
878
878
  'richText.slash.text': msg('Text'),
879
879
  'richText.slash.empty': msg('No matching blocks'),
880
880
  'richText.slash.componentsGroup': msg('Components'),
881
+ // EC-388 — expand the editing surface (a roomy sheet for long prose).
882
+ 'richText.expand.expand': msg('Expand editor'),
883
+ 'richText.expand.collapse': msg('Collapse editor'),
884
+ 'richText.expand.title': msg('Rich text'),
885
+ 'richText.expand.editingInSheet': msg('Editing in the expanded view — click to return'),
881
886
  // ─────────────────────────────────────────────────────────────────────────
882
887
  // EC-199 — AI assistant chat panel + creation flow
883
888
  // ─────────────────────────────────────────────────────────────────────────