@elytracms/studio 0.0.19 → 0.0.21

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 (41) hide show
  1. package/dist/components/assets/asset-metadata.d.ts +9 -0
  2. package/dist/components/assets/asset-metadata.js +3 -0
  3. package/dist/components/assets/asset-metadata.js.map +1 -1
  4. package/dist/components/assets/asset-thumb.d.ts +28 -0
  5. package/dist/components/assets/asset-thumb.js +55 -0
  6. package/dist/components/assets/asset-thumb.js.map +1 -0
  7. package/dist/components/assets/focal-point.d.ts +38 -0
  8. package/dist/components/assets/focal-point.js +47 -0
  9. package/dist/components/assets/focal-point.js.map +1 -0
  10. package/dist/components/builder/builder-store.d.ts +12 -0
  11. package/dist/components/builder/builder-store.js +15 -1
  12. package/dist/components/builder/builder-store.js.map +1 -1
  13. package/dist/components/builder/canvas.d.ts +7 -1
  14. package/dist/components/builder/canvas.js +63 -6
  15. package/dist/components/builder/canvas.js.map +1 -1
  16. package/dist/components/builder/composition-field-editor.js +3 -2
  17. package/dist/components/builder/composition-field-editor.js.map +1 -1
  18. package/dist/components/builder/page-tree.js +8 -2
  19. package/dist/components/builder/page-tree.js.map +1 -1
  20. package/dist/components/builder/reuse-context.d.ts +28 -0
  21. package/dist/components/builder/reuse-context.js +128 -0
  22. package/dist/components/builder/reuse-context.js.map +1 -0
  23. package/dist/components/builder/reuse.d.ts +59 -0
  24. package/dist/components/builder/reuse.js +117 -0
  25. package/dist/components/builder/reuse.js.map +1 -0
  26. package/dist/components/cms/cms-store.js +29 -6
  27. package/dist/components/cms/cms-store.js.map +1 -1
  28. package/dist/components/cms/detail-editor.js +78 -7
  29. package/dist/components/cms/detail-editor.js.map +1 -1
  30. package/dist/components/cms/field-control.d.ts +32 -1
  31. package/dist/components/cms/field-control.js +216 -17
  32. package/dist/components/cms/field-control.js.map +1 -1
  33. package/dist/components/section-nav.js +37 -7
  34. package/dist/components/section-nav.js.map +1 -1
  35. package/dist/lib/data/collection-content.d.ts +12 -3
  36. package/dist/lib/data/collection-content.js +20 -17
  37. package/dist/lib/data/collection-content.js.map +1 -1
  38. package/dist/lib/rail.d.ts +10 -0
  39. package/dist/lib/rail.js +25 -0
  40. package/dist/lib/rail.js.map +1 -1
  41. package/package.json +2 -2
@@ -1,8 +1,9 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- import { Fragment } from 'react';
2
+ import { Fragment, useCallback, useEffect, useState } from 'react';
3
3
  import { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router';
4
- import { AlertTriangle, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
4
+ import { AlertTriangle, ChevronDown, PanelLeftClose, PanelLeftOpen } from 'lucide-react';
5
5
  import { cn } from '@elytracms/studio/lib/utils';
6
+ import { readCollapsedViews, storeCollapsedViews } from '@elytracms/studio/lib/rail';
6
7
  import { Text } from '@elytracms/studio/ui';
7
8
  import { activeViewIdFromPath, useStudioNav } from './app-views/sidebar';
8
9
  import { useFieldRenderers } from './cms/field-renderer';
@@ -23,6 +24,24 @@ export function SectionNav({ project, collapsed, onToggleCollapsed }) {
23
24
  const { issues: rendererIssues } = useFieldRenderers();
24
25
  const issues = [...navIssues, ...rendererIssues];
25
26
  const projectId = project.id;
27
+ // EC-293: per-parent collapse of nested sub-views, persisted (read after mount,
28
+ // like the rail-collapse pref — no flash script needed). A collapsed parent hides
29
+ // its children but stays a navigable link.
30
+ const [collapsedViews, setCollapsedViews] = useState(() => new Set());
31
+ useEffect(() => {
32
+ setCollapsedViews(readCollapsedViews());
33
+ }, []);
34
+ const toggleViewCollapsed = useCallback((id) => {
35
+ setCollapsedViews((prev) => {
36
+ const next = new Set(prev);
37
+ if (next.has(id))
38
+ next.delete(id);
39
+ else
40
+ next.add(id);
41
+ storeCollapsedViews(next);
42
+ return next;
43
+ });
44
+ }, []);
26
45
  return (_jsxs("nav", { "aria-label": "Sections", className: cn('flex shrink-0 flex-col gap-0.5 border-r border-border bg-card/40 p-2 transition-[width] duration-150', collapsed ? 'w-[3.5rem]' : 'w-56'), children: [nodes.map((node, index) => {
27
46
  if (node.kind === 'separator') {
28
47
  return _jsx("div", { className: "my-1.5 border-t border-border/60", "aria-hidden": true }, `sep-${index}`);
@@ -38,23 +57,34 @@ export function SectionNav({ project, collapsed, onToggleCollapsed }) {
38
57
  if (node.availability === 'unavailable')
39
58
  return null;
40
59
  // EC-293: a view may nest its sub-views (CMS collections). The parent renders
41
- // as a link; its children render indented below (hidden when collapsed). The
42
- // parent gets a subtle active hint when one of its children is the active view.
60
+ // as a link with a disclosure chevron; its children render indented below,
61
+ // hidden when the rail is collapsed OR this parent is collapsed. The parent
62
+ // gets a subtle active hint when one of its children is the active view.
63
+ const hasChildren = (node.children?.length ?? 0) > 0;
43
64
  const hasActiveChild = node.children?.some((child) => child.id === activeViewId) ?? false;
44
- return (_jsxs(Fragment, { children: [_jsx(NavViewLink, { projectId: projectId, view: node, activeViewId: activeViewId, collapsed: collapsed, hasActiveChild: hasActiveChild }), collapsed
65
+ const viewCollapsed = collapsedViews.has(node.id);
66
+ return (_jsxs(Fragment, { children: [_jsx(NavViewLink, { projectId: projectId, view: node, activeViewId: activeViewId, collapsed: collapsed, hasActiveChild: hasActiveChild, collapsible: hasChildren && !collapsed, expanded: !viewCollapsed, onToggleExpanded: () => toggleViewCollapsed(node.id) }), collapsed || viewCollapsed
45
67
  ? null
46
68
  : node.children?.map((child) => (_jsx(NavViewLink, { projectId: projectId, view: child, activeViewId: activeViewId, collapsed: collapsed, nested: true }, child.id)))] }, node.id));
47
69
  }), _jsxs("div", { className: "mt-auto flex flex-col gap-0.5", children: [issues.length > 0 ? _jsx(NavIssues, { issues: issues, collapsed: collapsed }) : null, _jsx("button", { type: "button", onClick: onToggleCollapsed, "aria-label": collapsed ? 'Expand sidebar' : 'Collapse sidebar', "aria-pressed": collapsed, title: collapsed ? 'Expand sidebar' : 'Collapse sidebar', className: cn('flex h-9 items-center gap-2.5 rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground', collapsed ? 'justify-center px-0' : 'px-3'), children: collapsed ? (_jsx(PanelLeftOpen, { className: "size-4 shrink-0" })) : (_jsxs(_Fragment, { children: [_jsx(PanelLeftClose, { className: "size-4 shrink-0" }), _jsx("span", { className: "min-w-0 flex-1 truncate text-left", children: "Collapse" })] })) })] })] }));
48
70
  }
49
71
  /** One rail view (top-level or nested) — a Link to its home route, active by view id. */
50
- function NavViewLink({ projectId, view, activeViewId, collapsed, nested = false, hasActiveChild = false, }) {
72
+ function NavViewLink({ projectId, view, activeViewId, collapsed, nested = false, hasActiveChild = false, collapsible = false, expanded = true, onToggleExpanded, }) {
51
73
  const isActive = view.id === activeViewId;
52
74
  const Icon = view.icon;
53
- return (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: view.path }, title: view.label, "aria-current": isActive ? 'page' : undefined, className: cn('group relative flex h-9 items-center gap-2.5 rounded-md text-sm transition-colors', collapsed ? 'justify-center px-0' : nested ? 'pl-7 pr-3' : 'px-3', isActive
75
+ const link = (_jsxs(Link, { to: STUDIO_PROJECT_ROUTE, params: { projectId, _splat: view.path }, title: view.label, "aria-current": isActive ? 'page' : undefined, className: cn('group relative flex h-9 items-center gap-2.5 rounded-md text-sm transition-colors',
76
+ // Collapsible parents reserve room on the right for the chevron toggle.
77
+ collapsed ? 'justify-center px-0' : nested ? 'pl-7 pr-3' : collapsible ? 'pl-3 pr-9' : 'px-3', isActive
54
78
  ? 'bg-accent font-medium text-accent-foreground'
55
79
  : hasActiveChild
56
80
  ? 'text-foreground hover:bg-muted'
57
81
  : 'text-muted-foreground hover:bg-muted hover:text-foreground'), children: [isActive ? (_jsx("span", { className: "absolute inset-y-1.5 left-0 w-0.5 rounded-full bg-primary", "aria-hidden": true })) : null, _jsx(Icon, { className: "size-4 shrink-0" }), collapsed ? null : _jsx("span", { className: "min-w-0 flex-1 truncate", children: view.label }), view.availability === 'planned' && !collapsed ? (_jsx("span", { className: "size-1.5 shrink-0 rounded-full bg-warning", "aria-hidden": true, title: "Planned" })) : null] }));
82
+ // A leaf (or the collapsed icons-only rail) is just the link.
83
+ if (!collapsible)
84
+ return link;
85
+ // A parent pairs the link with a sibling chevron toggle (so the chevron collapses
86
+ // without navigating, and the link still navigates to the parent surface).
87
+ return (_jsxs("div", { className: "relative", children: [link, _jsx("button", { type: "button", onClick: onToggleExpanded, "aria-label": `${expanded ? 'Collapse' : 'Expand'} ${view.label}`, "aria-expanded": expanded, className: "absolute right-1 top-1/2 grid size-6 -translate-y-1/2 place-items-center rounded text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground", children: _jsx(ChevronDown, { className: cn('size-3.5 transition-transform', expanded ? '' : '-rotate-90'), "aria-hidden": true }) })] }));
58
88
  }
59
89
  /** Visible customization issues at the rail foot — the seam's "never silent" (EC-236). */
60
90
  function NavIssues({ issues, collapsed }) {
@@ -1 +1 @@
1
- {"version":3,"file":"section-nav.js","sourceRoot":"","sources":["../../src/components/section-nav.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Fragment } from 'react'\nimport { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { AlertTriangle, PanelLeftClose, PanelLeftOpen } from 'lucide-react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { Text } from '@elytracms/studio/ui'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport type { ResolvedRailView } from './app-views/sidebar'\nimport { useFieldRenderers } from './cms/field-renderer'\n\nexport interface SectionNavProps {\n project: Project\n /** User-toggled collapse (EC-214) — icons only when collapsed, persisted by the shell. */\n collapsed: boolean\n onToggleCollapsed: () => void\n}\n\n/**\n * Vertical section rail (brief §8.1), driven by the configurable sidebar (EC-237):\n * a consumer-extendable tree of views / groups / separators resolved over the\n * built-in views (EC-239). Availability-aware (`unavailable` items are absent per\n * EC-221); the active item is matched by the VIEW ID in the URL, so two views\n * backed by the same surface (Content vs. a focused Categories) highlight\n * independently. The preference-persisted collapse and the strong active marker\n * (EC-214) are preserved. Customization issues (duplicate view id, invalid view,\n * non-component renderers) render at the rail foot — visible, never silent.\n */\nexport function SectionNav({ project, collapsed, onToggleCollapsed }: SectionNavProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n const { nodes, issues: navIssues } = useStudioNav(project)\n const { issues: rendererIssues } = useFieldRenderers()\n const issues = [...navIssues, ...rendererIssues]\n const projectId = project.id\n\n return (\n <nav\n aria-label=\"Sections\"\n className={cn(\n 'flex shrink-0 flex-col gap-0.5 border-r border-border bg-card/40 p-2 transition-[width] duration-150',\n collapsed ? 'w-[3.5rem]' : 'w-56',\n )}\n >\n {nodes.map((node, index) => {\n if (node.kind === 'separator') {\n return <div key={`sep-${index}`} className=\"my-1.5 border-t border-border/60\" aria-hidden />\n }\n if (node.kind === 'group') {\n const views = node.views.filter((view) => view.availability !== 'unavailable')\n if (views.length === 0) return null\n return (\n <div key={`group-${node.label}-${index}`} className=\"mt-2 first:mt-0\">\n {collapsed ? (\n <div className=\"my-1 border-t border-border/60\" aria-hidden />\n ) : (\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"px-3 pb-1 pt-1 font-medium uppercase tracking-wide\"\n >\n {node.label}\n </Text>\n )}\n {views.map((view) => (\n <NavViewLink\n key={view.id}\n projectId={projectId}\n view={view}\n activeViewId={activeViewId}\n collapsed={collapsed}\n />\n ))}\n </div>\n )\n }\n // EC-221: an unavailable view is absent from the rail (not greyed) — it\n // stays reachable by URL, the rail simply doesn't advertise it.\n if (node.availability === 'unavailable') return null\n // EC-293: a view may nest its sub-views (CMS collections). The parent renders\n // as a link; its children render indented below (hidden when collapsed). The\n // parent gets a subtle active hint when one of its children is the active view.\n const hasActiveChild = node.children?.some((child) => child.id === activeViewId) ?? false\n return (\n <Fragment key={node.id}>\n <NavViewLink\n projectId={projectId}\n view={node}\n activeViewId={activeViewId}\n collapsed={collapsed}\n hasActiveChild={hasActiveChild}\n />\n {collapsed\n ? null\n : node.children?.map((child) => (\n <NavViewLink\n key={child.id}\n projectId={projectId}\n view={child}\n activeViewId={activeViewId}\n collapsed={collapsed}\n nested\n />\n ))}\n </Fragment>\n )\n })}\n\n <div className=\"mt-auto flex flex-col gap-0.5\">\n {issues.length > 0 ? <NavIssues issues={issues} collapsed={collapsed} /> : null}\n\n {/* Collapse toggle pinned to the rail foot (EC-214). */}\n <button\n type=\"button\"\n onClick={onToggleCollapsed}\n aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n aria-pressed={collapsed}\n title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n className={cn(\n 'flex h-9 items-center gap-2.5 rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n collapsed ? 'justify-center px-0' : 'px-3',\n )}\n >\n {collapsed ? (\n <PanelLeftOpen className=\"size-4 shrink-0\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">Collapse</span>\n </>\n )}\n </button>\n </div>\n </nav>\n )\n}\n\n/** One rail view (top-level or nested) — a Link to its home route, active by view id. */\nfunction NavViewLink({\n projectId,\n view,\n activeViewId,\n collapsed,\n nested = false,\n hasActiveChild = false,\n}: {\n projectId: string\n view: ResolvedRailView\n activeViewId: string | null\n collapsed: boolean\n nested?: boolean\n /** EC-293: this parent's a child is the active view → a subtle (non-accent) hint. */\n hasActiveChild?: boolean\n}) {\n const isActive = view.id === activeViewId\n const Icon = view.icon\n return (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.path }}\n title={view.label}\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n 'group relative flex h-9 items-center gap-2.5 rounded-md text-sm transition-colors',\n collapsed ? 'justify-center px-0' : nested ? 'pl-7 pr-3' : 'px-3',\n isActive\n ? 'bg-accent font-medium text-accent-foreground'\n : hasActiveChild\n ? 'text-foreground hover:bg-muted'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {/* Strong active marker: a leading accent bar (EC-214). */}\n {isActive ? (\n <span className=\"absolute inset-y-1.5 left-0 w-0.5 rounded-full bg-primary\" aria-hidden />\n ) : null}\n <Icon className=\"size-4 shrink-0\" />\n {collapsed ? null : <span className=\"min-w-0 flex-1 truncate\">{view.label}</span>}\n {view.availability === 'planned' && !collapsed ? (\n <span className=\"size-1.5 shrink-0 rounded-full bg-warning\" aria-hidden title=\"Planned\" />\n ) : null}\n </Link>\n )\n}\n\n/** Visible customization issues at the rail foot — the seam's \"never silent\" (EC-236). */\nfunction NavIssues({ issues, collapsed }: { issues: CustomizationIssue[]; collapsed: boolean }) {\n if (collapsed) {\n return (\n <div\n className=\"grid h-9 place-items-center text-amber-600 dark:text-amber-400\"\n title={issues.map((issue) => issue.message).join('\\n')}\n >\n <AlertTriangle className=\"size-4\" aria-label={`${issues.length} sidebar issues`} />\n </div>\n )\n }\n return (\n <div className=\"mb-1 flex flex-col gap-1 rounded-md border border-amber-500/30 bg-amber-500/5 p-2\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.point}:${issue.id ?? index}`}\n className=\"flex items-start gap-1.5 text-amber-600 dark:text-amber-400\"\n title={issue.message}\n >\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 leading-snug\">\n {issue.message}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"section-nav.js","sourceRoot":"","sources":["../../src/components/section-nav.tsx"],"names":[],"mappings":"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","sourcesContent":["import { Fragment, useCallback, useEffect, useState } from 'react'\nimport { Link, STUDIO_PROJECT_ROUTE, useLocation } from '@elytracms/studio/router'\nimport { AlertTriangle, ChevronDown, PanelLeftClose, PanelLeftOpen } from 'lucide-react'\nimport type { Project } from '@elytracms/core/studio-core'\nimport type { CustomizationIssue } from '@elytracms/core/studio-core'\nimport { cn } from '@elytracms/studio/lib/utils'\nimport { readCollapsedViews, storeCollapsedViews } from '@elytracms/studio/lib/rail'\nimport { Text } from '@elytracms/studio/ui'\nimport { activeViewIdFromPath, useStudioNav } from './app-views/sidebar'\nimport type { ResolvedRailView } from './app-views/sidebar'\nimport { useFieldRenderers } from './cms/field-renderer'\n\nexport interface SectionNavProps {\n project: Project\n /** User-toggled collapse (EC-214) — icons only when collapsed, persisted by the shell. */\n collapsed: boolean\n onToggleCollapsed: () => void\n}\n\n/**\n * Vertical section rail (brief §8.1), driven by the configurable sidebar (EC-237):\n * a consumer-extendable tree of views / groups / separators resolved over the\n * built-in views (EC-239). Availability-aware (`unavailable` items are absent per\n * EC-221); the active item is matched by the VIEW ID in the URL, so two views\n * backed by the same surface (Content vs. a focused Categories) highlight\n * independently. The preference-persisted collapse and the strong active marker\n * (EC-214) are preserved. Customization issues (duplicate view id, invalid view,\n * non-component renderers) render at the rail foot — visible, never silent.\n */\nexport function SectionNav({ project, collapsed, onToggleCollapsed }: SectionNavProps) {\n const { pathname } = useLocation()\n const activeViewId = activeViewIdFromPath(pathname)\n const { nodes, issues: navIssues } = useStudioNav(project)\n const { issues: rendererIssues } = useFieldRenderers()\n const issues = [...navIssues, ...rendererIssues]\n const projectId = project.id\n\n // EC-293: per-parent collapse of nested sub-views, persisted (read after mount,\n // like the rail-collapse pref — no flash script needed). A collapsed parent hides\n // its children but stays a navigable link.\n const [collapsedViews, setCollapsedViews] = useState<Set<string>>(() => new Set())\n useEffect(() => {\n setCollapsedViews(readCollapsedViews())\n }, [])\n const toggleViewCollapsed = useCallback((id: string) => {\n setCollapsedViews((prev) => {\n const next = new Set(prev)\n if (next.has(id)) next.delete(id)\n else next.add(id)\n storeCollapsedViews(next)\n return next\n })\n }, [])\n\n return (\n <nav\n aria-label=\"Sections\"\n className={cn(\n 'flex shrink-0 flex-col gap-0.5 border-r border-border bg-card/40 p-2 transition-[width] duration-150',\n collapsed ? 'w-[3.5rem]' : 'w-56',\n )}\n >\n {nodes.map((node, index) => {\n if (node.kind === 'separator') {\n return <div key={`sep-${index}`} className=\"my-1.5 border-t border-border/60\" aria-hidden />\n }\n if (node.kind === 'group') {\n const views = node.views.filter((view) => view.availability !== 'unavailable')\n if (views.length === 0) return null\n return (\n <div key={`group-${node.label}-${index}`} className=\"mt-2 first:mt-0\">\n {collapsed ? (\n <div className=\"my-1 border-t border-border/60\" aria-hidden />\n ) : (\n <Text\n size=\"xs\"\n tone=\"subtle\"\n className=\"px-3 pb-1 pt-1 font-medium uppercase tracking-wide\"\n >\n {node.label}\n </Text>\n )}\n {views.map((view) => (\n <NavViewLink\n key={view.id}\n projectId={projectId}\n view={view}\n activeViewId={activeViewId}\n collapsed={collapsed}\n />\n ))}\n </div>\n )\n }\n // EC-221: an unavailable view is absent from the rail (not greyed) — it\n // stays reachable by URL, the rail simply doesn't advertise it.\n if (node.availability === 'unavailable') return null\n // EC-293: a view may nest its sub-views (CMS collections). The parent renders\n // as a link with a disclosure chevron; its children render indented below,\n // hidden when the rail is collapsed OR this parent is collapsed. The parent\n // gets a subtle active hint when one of its children is the active view.\n const hasChildren = (node.children?.length ?? 0) > 0\n const hasActiveChild = node.children?.some((child) => child.id === activeViewId) ?? false\n const viewCollapsed = collapsedViews.has(node.id)\n return (\n <Fragment key={node.id}>\n <NavViewLink\n projectId={projectId}\n view={node}\n activeViewId={activeViewId}\n collapsed={collapsed}\n hasActiveChild={hasActiveChild}\n collapsible={hasChildren && !collapsed}\n expanded={!viewCollapsed}\n onToggleExpanded={() => toggleViewCollapsed(node.id)}\n />\n {collapsed || viewCollapsed\n ? null\n : node.children?.map((child) => (\n <NavViewLink\n key={child.id}\n projectId={projectId}\n view={child}\n activeViewId={activeViewId}\n collapsed={collapsed}\n nested\n />\n ))}\n </Fragment>\n )\n })}\n\n <div className=\"mt-auto flex flex-col gap-0.5\">\n {issues.length > 0 ? <NavIssues issues={issues} collapsed={collapsed} /> : null}\n\n {/* Collapse toggle pinned to the rail foot (EC-214). */}\n <button\n type=\"button\"\n onClick={onToggleCollapsed}\n aria-label={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n aria-pressed={collapsed}\n title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}\n className={cn(\n 'flex h-9 items-center gap-2.5 rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground',\n collapsed ? 'justify-center px-0' : 'px-3',\n )}\n >\n {collapsed ? (\n <PanelLeftOpen className=\"size-4 shrink-0\" />\n ) : (\n <>\n <PanelLeftClose className=\"size-4 shrink-0\" />\n <span className=\"min-w-0 flex-1 truncate text-left\">Collapse</span>\n </>\n )}\n </button>\n </div>\n </nav>\n )\n}\n\n/** One rail view (top-level or nested) — a Link to its home route, active by view id. */\nfunction NavViewLink({\n projectId,\n view,\n activeViewId,\n collapsed,\n nested = false,\n hasActiveChild = false,\n collapsible = false,\n expanded = true,\n onToggleExpanded,\n}: {\n projectId: string\n view: ResolvedRailView\n activeViewId: string | null\n collapsed: boolean\n nested?: boolean\n /** EC-293: this parent's a child is the active view → a subtle (non-accent) hint. */\n hasActiveChild?: boolean\n /** EC-293: this parent has nested children → render a disclosure chevron. */\n collapsible?: boolean\n /** Whether the nested children are shown (chevron points down) or hidden. */\n expanded?: boolean\n /** Toggle this parent's collapse (the chevron click — does not navigate). */\n onToggleExpanded?: () => void\n}) {\n const isActive = view.id === activeViewId\n const Icon = view.icon\n const link = (\n <Link\n to={STUDIO_PROJECT_ROUTE}\n params={{ projectId, _splat: view.path }}\n title={view.label}\n aria-current={isActive ? 'page' : undefined}\n className={cn(\n 'group relative flex h-9 items-center gap-2.5 rounded-md text-sm transition-colors',\n // Collapsible parents reserve room on the right for the chevron toggle.\n collapsed ? 'justify-center px-0' : nested ? 'pl-7 pr-3' : collapsible ? 'pl-3 pr-9' : 'px-3',\n isActive\n ? 'bg-accent font-medium text-accent-foreground'\n : hasActiveChild\n ? 'text-foreground hover:bg-muted'\n : 'text-muted-foreground hover:bg-muted hover:text-foreground',\n )}\n >\n {/* Strong active marker: a leading accent bar (EC-214). */}\n {isActive ? (\n <span className=\"absolute inset-y-1.5 left-0 w-0.5 rounded-full bg-primary\" aria-hidden />\n ) : null}\n <Icon className=\"size-4 shrink-0\" />\n {collapsed ? null : <span className=\"min-w-0 flex-1 truncate\">{view.label}</span>}\n {view.availability === 'planned' && !collapsed ? (\n <span className=\"size-1.5 shrink-0 rounded-full bg-warning\" aria-hidden title=\"Planned\" />\n ) : null}\n </Link>\n )\n // A leaf (or the collapsed icons-only rail) is just the link.\n if (!collapsible) return link\n // A parent pairs the link with a sibling chevron toggle (so the chevron collapses\n // without navigating, and the link still navigates to the parent surface).\n return (\n <div className=\"relative\">\n {link}\n <button\n type=\"button\"\n onClick={onToggleExpanded}\n aria-label={`${expanded ? 'Collapse' : 'Expand'} ${view.label}`}\n aria-expanded={expanded}\n className=\"absolute right-1 top-1/2 grid size-6 -translate-y-1/2 place-items-center rounded text-muted-foreground/70 transition-colors hover:bg-background hover:text-foreground\"\n >\n <ChevronDown\n className={cn('size-3.5 transition-transform', expanded ? '' : '-rotate-90')}\n aria-hidden\n />\n </button>\n </div>\n )\n}\n\n/** Visible customization issues at the rail foot — the seam's \"never silent\" (EC-236). */\nfunction NavIssues({ issues, collapsed }: { issues: CustomizationIssue[]; collapsed: boolean }) {\n if (collapsed) {\n return (\n <div\n className=\"grid h-9 place-items-center text-amber-600 dark:text-amber-400\"\n title={issues.map((issue) => issue.message).join('\\n')}\n >\n <AlertTriangle className=\"size-4\" aria-label={`${issues.length} sidebar issues`} />\n </div>\n )\n }\n return (\n <div className=\"mb-1 flex flex-col gap-1 rounded-md border border-amber-500/30 bg-amber-500/5 p-2\">\n {issues.map((issue, index) => (\n <div\n key={`${issue.point}:${issue.id ?? index}`}\n className=\"flex items-start gap-1.5 text-amber-600 dark:text-amber-400\"\n title={issue.message}\n >\n <AlertTriangle className=\"mt-0.5 size-3.5 shrink-0\" aria-hidden />\n <Text size=\"xs\" className=\"min-w-0 leading-snug\">\n {issue.message}\n </Text>\n </div>\n ))}\n </div>\n )\n}\n"]}
@@ -58,6 +58,15 @@ export interface ContentSessionState {
58
58
  * collections + documents arrive via {@link configureContentSession} (Convex mode).
59
59
  */
60
60
  export declare const playgroundCollections: readonly CollectionDef[];
61
+ /**
62
+ * The perspective a canvas preview resolves references against (EC-300/AD-16). The
63
+ * canvas is draft-coherent: the editable Draft view resolves references to their
64
+ * `'draft'` targets (so a just-made reusable shows immediately), the read-only
65
+ * Published view resolves `'published'` (an honest preview of what delivery serves;
66
+ * an unpublished hard ref shows the in-canvas placeholder). Delivery stays
67
+ * published-only — this does not weaken `live-is-published-only`.
68
+ */
69
+ export type CanvasPerspective = 'draft' | 'published';
61
70
  /**
62
71
  * Point the preview engine at a project's hydrated content (Convex mode), or
63
72
  * back at the deterministic playground fixtures (`null` — the default, and
@@ -93,7 +102,7 @@ export declare function getCollectionDocument(collectionId: string, idOrSlug: st
93
102
  * Reusable's element) exactly as delivery renders them. `null` when the target is
94
103
  * missing or not visible → the block degrades to its own empty state.
95
104
  */
96
- export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale): ResolvedDocument | null;
105
+ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale, perspective?: CanvasPerspective): ResolvedDocument | null;
97
106
  /**
98
107
  * Resolve a listing block's query to its documents for the canvas (EC-255) — the
99
108
  * studio analog of the host's baked listings map. Runs the query (collection +
@@ -103,7 +112,7 @@ export declare function resolveCanvasRelation(ref: DocumentRef, locale?: Locale)
103
112
  * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty
104
113
  * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.
105
114
  */
106
- export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale): ResolvedDocument[];
115
+ export declare function resolveCanvasListing(query: ListingQuery, locale?: Locale, perspective?: CanvasPerspective): ResolvedDocument[];
107
116
  /**
108
117
  * Resolve a rich-text internal link's target document to its URL path for the canvas
109
118
  * (EC-291) — the studio analog of the host's baked links map. Derives the URL through
@@ -113,7 +122,7 @@ export declare function resolveCanvasListing(query: ListingQuery, locale?: Local
113
122
  * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
114
123
  * the same `resolveLink` path as the host bake, so both agree on the derived URL.
115
124
  */
116
- export declare function resolveCanvasLink(ref: DocumentRef, locale?: Locale): string | null;
125
+ export declare function resolveCanvasLink(ref: DocumentRef, locale?: Locale, perspective?: CanvasPerspective): string | null;
117
126
  /** A pickable entry of a collection (the template-page preview switcher). */
118
127
  export interface CollectionEntrySummary {
119
128
  id: string;
@@ -10,6 +10,7 @@ import { getStudioLocale } from '../locale/locale-store';
10
10
  export const playgroundCollections = [];
11
11
  let contentSession = null;
12
12
  let lookup = createLookup(playgroundCollections, [], []);
13
+ // Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.
13
14
  const clients = new Map();
14
15
  /**
15
16
  * Point the preview engine at a project's hydrated content (Convex mode), or
@@ -121,28 +122,30 @@ function createLookup(collectionDefs, documents, publishedKeys, assetRecords) {
121
122
  };
122
123
  }
123
124
  /**
124
- * One deterministic client per locale: published perspective, EC-018 locale
125
- * fallback. The studio's ambient active locale (EC-176, `lib/locale`) picks
126
- * the client at query time, so the canvas previews follow the locale switcher
127
- * exactly like delivery would. The cache resets whenever the content session changes.
125
+ * One deterministic client per (locale, perspective): EC-018 locale fallback,
126
+ * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's
127
+ * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the
128
+ * canvas previews follow the locale switcher exactly like delivery would; the canvas's
129
+ * Draft/Published view picks the perspective. The cache resets whenever the content
130
+ * session changes.
128
131
  */
129
- function clientFor(locale) {
130
- const existing = clients.get(locale);
132
+ function clientFor(locale, perspective = 'published') {
133
+ const key = `${locale}:${perspective}`;
134
+ const existing = clients.get(key);
131
135
  if (existing)
132
136
  return existing;
133
137
  const client = createContentClient({
134
138
  lookup,
135
139
  context: createContentContext({
136
140
  locale,
137
- perspective: 'published',
141
+ perspective,
138
142
  locales: activeLocaleConfig(),
139
143
  }),
140
- // EC-291: the routes a rich-text link resolves against (config-sourced). The
141
- // canvas previews PUBLISHED content, so a link derives the live URL — matching
142
- // the host's baked link map.
144
+ // EC-291: the routes a rich-text link resolves against (config-sourced) — the
145
+ // studio-live half of the host link bake.
143
146
  routes: contentSession?.routes ?? [],
144
147
  });
145
- clients.set(locale, client);
148
+ clients.set(key, client);
146
149
  return client;
147
150
  }
148
151
  /**
@@ -166,8 +169,8 @@ export function getCollectionDocument(collectionId, idOrSlug, locale = getStudio
166
169
  * Reusable's element) exactly as delivery renders them. `null` when the target is
167
170
  * missing or not visible → the block degrades to its own empty state.
168
171
  */
169
- export function resolveCanvasRelation(ref, locale = getStudioLocale()) {
170
- return clientFor(locale).resolveRelation(ref);
172
+ export function resolveCanvasRelation(ref, locale = getStudioLocale(), perspective = 'published') {
173
+ return clientFor(locale, perspective).resolveRelation(ref);
171
174
  }
172
175
  /**
173
176
  * Resolve a listing block's query to its documents for the canvas (EC-255) — the
@@ -178,8 +181,8 @@ export function resolveCanvasRelation(ref, locale = getStudioLocale()) {
178
181
  * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty
179
182
  * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.
180
183
  */
181
- export function resolveCanvasListing(query, locale = getStudioLocale()) {
182
- const result = clientFor(locale).listDocuments(query.collection, {
184
+ export function resolveCanvasListing(query, locale = getStudioLocale(), perspective = 'published') {
185
+ const result = clientFor(locale, perspective).listDocuments(query.collection, {
183
186
  where: query.where,
184
187
  ...(query.sort ? { sort: query.sort } : {}),
185
188
  ...(query.limit !== undefined ? { limit: query.limit } : {}),
@@ -195,8 +198,8 @@ export function resolveCanvasListing(query, locale = getStudioLocale()) {
195
198
  * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses
196
199
  * the same `resolveLink` path as the host bake, so both agree on the derived URL.
197
200
  */
198
- export function resolveCanvasLink(ref, locale = getStudioLocale()) {
199
- return clientFor(locale).resolveLink(ref);
201
+ export function resolveCanvasLink(ref, locale = getStudioLocale(), perspective = 'published') {
202
+ return clientFor(locale, perspective).resolveLink(ref);
200
203
  }
201
204
  /** Published entries of a collection, labeled by the schema's title field. */
202
205
  export function listCollectionEntries(collectionId) {
@@ -1 +1 @@
1
- {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Playground mode (and tests): the deterministic EC-145 sample fixtures —\n * byte-identical to the pre-hydration behavior, no backend.\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n *\n * Nothing here invents query semantics; both shapes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} (Convex mode).\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\nconst clients = new Map<Locale, ContentClient>()\n\n/**\n * Point the preview engine at a project's hydrated content (Convex mode), or\n * back at the deterministic playground fixtures (`null` — the default, and\n * the only state playground mode ever sees).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per locale: published perspective, EC-018 locale\n * fallback. The studio's ambient active locale (EC-176, `lib/locale`) picks\n * the client at query time, so the canvas previews follow the locale switcher\n * exactly like delivery would. The cache resets whenever the content session changes.\n */\nfunction clientFor(locale: Locale): ContentClient {\n const existing = clients.get(locale)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective: 'published',\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced). The\n // canvas previews PUBLISHED content, so a link derives the live URL — matching\n // the host's baked link map.\n routes: contentSession?.routes ?? [],\n })\n clients.set(locale, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n): ResolvedDocument | null {\n return clientFor(locale).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n): ResolvedDocument[] {\n const result = clientFor(locale).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n): string | null {\n return clientFor(locale).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n return result.documents.map((doc) => {\n const title = collection?.titleField ? doc[collection.titleField] : undefined\n return { id: doc.id, label: typeof title === 'string' && title.length > 0 ? title : doc.id }\n })\n}\n"]}
1
+ {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Playground mode (and tests): the deterministic EC-145 sample fixtures —\n * byte-identical to the pre-hydration behavior, no backend.\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n *\n * Nothing here invents query semantics; both shapes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} (Convex mode).\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n/**\n * Point the preview engine at a project's hydrated content (Convex mode), or\n * back at the deterministic playground fixtures (`null` — the default, and\n * the only state playground mode ever sees).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n return result.documents.map((doc) => {\n const title = collection?.titleField ? doc[collection.titleField] : undefined\n return { id: doc.id, label: typeof title === 'string' && title.length > 0 ? title : doc.id }\n })\n}\n"]}
@@ -9,3 +9,13 @@ export declare const RAIL_STORAGE_KEY = "elytra-rail-collapsed";
9
9
  export declare function readRailCollapsed(): boolean;
10
10
  /** Persist the collapse preference (best-effort; storage may be unavailable). */
11
11
  export declare function storeRailCollapsed(collapsed: boolean): void;
12
+ /**
13
+ * Per-parent collapse of the rail's nested sub-views (EC-293). We persist the
14
+ * exceptions — the set of COLLAPSED parent view ids — so the default (every parent
15
+ * expanded) needs no storage and an unknown/removed id is harmless.
16
+ */
17
+ export declare const RAIL_COLLAPSED_VIEWS_KEY = "elytra-rail-collapsed-views";
18
+ /** Read the collapsed parent-view ids; empty (all expanded) when unset/unavailable. */
19
+ export declare function readCollapsedViews(): Set<string>;
20
+ /** Persist the collapsed parent-view ids (best-effort; storage may be unavailable). */
21
+ export declare function storeCollapsedViews(ids: ReadonlySet<string>): void;
package/dist/lib/rail.js CHANGED
@@ -23,4 +23,29 @@ export function storeRailCollapsed(collapsed) {
23
23
  // ignore — preference simply won't persist this session
24
24
  }
25
25
  }
26
+ /**
27
+ * Per-parent collapse of the rail's nested sub-views (EC-293). We persist the
28
+ * exceptions — the set of COLLAPSED parent view ids — so the default (every parent
29
+ * expanded) needs no storage and an unknown/removed id is harmless.
30
+ */
31
+ export const RAIL_COLLAPSED_VIEWS_KEY = 'elytra-rail-collapsed-views';
32
+ /** Read the collapsed parent-view ids; empty (all expanded) when unset/unavailable. */
33
+ export function readCollapsedViews() {
34
+ try {
35
+ const raw = localStorage.getItem(RAIL_COLLAPSED_VIEWS_KEY);
36
+ return new Set(raw ? raw.split(',').filter(Boolean) : []);
37
+ }
38
+ catch {
39
+ return new Set();
40
+ }
41
+ }
42
+ /** Persist the collapsed parent-view ids (best-effort; storage may be unavailable). */
43
+ export function storeCollapsedViews(ids) {
44
+ try {
45
+ localStorage.setItem(RAIL_COLLAPSED_VIEWS_KEY, [...ids].join(','));
46
+ }
47
+ catch {
48
+ // ignore — preference simply won't persist this session
49
+ }
50
+ }
26
51
  //# sourceMappingURL=rail.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"rail.js","sourceRoot":"","sources":["../../src/lib/rail.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAA;AAEvD,uFAAuF;AACvF,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,GAAG,CAAA;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,kBAAkB,CAAC,SAAkB;IACnD,IAAI,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;IAC1D,CAAC;AACH,CAAC","sourcesContent":["/**\n * Section-rail collapse preference (EC-214). The studio's vertical section rail\n * is user-collapsible; the choice persists across sessions so the shell opens\n * the way the user left it. Best-effort like the theme preference: storage may be\n * unavailable, in which case the rail simply defaults to expanded each session.\n */\nexport const RAIL_STORAGE_KEY = 'elytra-rail-collapsed'\n\n/** Read the persisted collapse preference; expanded (false) when unset/unavailable. */\nexport function readRailCollapsed(): boolean {\n try {\n return localStorage.getItem(RAIL_STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\n/** Persist the collapse preference (best-effort; storage may be unavailable). */\nexport function storeRailCollapsed(collapsed: boolean): void {\n try {\n localStorage.setItem(RAIL_STORAGE_KEY, collapsed ? '1' : '0')\n } catch {\n // ignore — preference simply won't persist this session\n }\n}\n"]}
1
+ {"version":3,"file":"rail.js","sourceRoot":"","sources":["../../src/lib/rail.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,uBAAuB,CAAA;AAEvD,uFAAuF;AACvF,MAAM,UAAU,iBAAiB;IAC/B,IAAI,CAAC;QACH,OAAO,YAAY,CAAC,OAAO,CAAC,gBAAgB,CAAC,KAAK,GAAG,CAAA;IACvD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAA;IACd,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,kBAAkB,CAAC,SAAkB;IACnD,IAAI,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAA;IAC/D,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;IAC1D,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,6BAA6B,CAAA;AAErE,uFAAuF;AACvF,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,OAAO,CAAC,wBAAwB,CAAC,CAAA;QAC1D,OAAO,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC3D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,GAAG,EAAE,CAAA;IAClB,CAAC;AACH,CAAC;AAED,uFAAuF;AACvF,MAAM,UAAU,mBAAmB,CAAC,GAAwB;IAC1D,IAAI,CAAC;QACH,YAAY,CAAC,OAAO,CAAC,wBAAwB,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,wDAAwD;IAC1D,CAAC;AACH,CAAC","sourcesContent":["/**\n * Section-rail collapse preference (EC-214). The studio's vertical section rail\n * is user-collapsible; the choice persists across sessions so the shell opens\n * the way the user left it. Best-effort like the theme preference: storage may be\n * unavailable, in which case the rail simply defaults to expanded each session.\n */\nexport const RAIL_STORAGE_KEY = 'elytra-rail-collapsed'\n\n/** Read the persisted collapse preference; expanded (false) when unset/unavailable. */\nexport function readRailCollapsed(): boolean {\n try {\n return localStorage.getItem(RAIL_STORAGE_KEY) === '1'\n } catch {\n return false\n }\n}\n\n/** Persist the collapse preference (best-effort; storage may be unavailable). */\nexport function storeRailCollapsed(collapsed: boolean): void {\n try {\n localStorage.setItem(RAIL_STORAGE_KEY, collapsed ? '1' : '0')\n } catch {\n // ignore — preference simply won't persist this session\n }\n}\n\n/**\n * Per-parent collapse of the rail's nested sub-views (EC-293). We persist the\n * exceptions — the set of COLLAPSED parent view ids — so the default (every parent\n * expanded) needs no storage and an unknown/removed id is harmless.\n */\nexport const RAIL_COLLAPSED_VIEWS_KEY = 'elytra-rail-collapsed-views'\n\n/** Read the collapsed parent-view ids; empty (all expanded) when unset/unavailable. */\nexport function readCollapsedViews(): Set<string> {\n try {\n const raw = localStorage.getItem(RAIL_COLLAPSED_VIEWS_KEY)\n return new Set(raw ? raw.split(',').filter(Boolean) : [])\n } catch {\n return new Set()\n }\n}\n\n/** Persist the collapsed parent-view ids (best-effort; storage may be unavailable). */\nexport function storeCollapsedViews(ids: ReadonlySet<string>): void {\n try {\n localStorage.setItem(RAIL_COLLAPSED_VIEWS_KEY, [...ids].join(','))\n } catch {\n // ignore — preference simply won't persist this session\n }\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elytracms/studio",
3
3
  "author": "finaldream <hi@finaldream.de>",
4
- "version": "0.0.19",
4
+ "version": "0.0.21",
5
5
  "private": false,
6
6
  "description": "The Elytra studio engine (UI + lib + routes + data-only Convex schema) as an importable package. Extraction staged in EXTRACTION.md (EC-223); Stages 1-2 = UI primitives + router-free lib leaves.",
7
7
  "license": "MIT",
@@ -117,7 +117,7 @@
117
117
  "lucide-react": "^1.17.0",
118
118
  "tailwind-merge": "^3.6.0",
119
119
  "zod": "^4.0.0",
120
- "@elytracms/core": "^0.0.19"
120
+ "@elytracms/core": "^0.0.21"
121
121
  },
122
122
  "peerDependencies": {
123
123
  "@tanstack/react-router": "^1.170.0",