@elytracms/studio 0.0.28 → 0.0.31

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 (64) hide show
  1. package/convex/redirects.ts +3 -0
  2. package/convex/schema.ts +2 -0
  3. package/dist/components/app-views/default-modules.d.ts +20 -3
  4. package/dist/components/app-views/default-modules.js +27 -19
  5. package/dist/components/app-views/default-modules.js.map +1 -1
  6. package/dist/components/app-views/studio-config.d.ts +10 -0
  7. package/dist/components/app-views/studio-config.js.map +1 -1
  8. package/dist/components/builder/property-inspector.js +3 -2
  9. package/dist/components/builder/property-inspector.js.map +1 -1
  10. package/dist/components/builder/reuse-context.js +2 -1
  11. package/dist/components/builder/reuse-context.js.map +1 -1
  12. package/dist/components/cms/cms-index.js +2 -1
  13. package/dist/components/cms/cms-index.js.map +1 -1
  14. package/dist/components/cms/cms-store.d.ts +11 -2
  15. package/dist/components/cms/cms-store.js +17 -4
  16. package/dist/components/cms/cms-store.js.map +1 -1
  17. package/dist/components/cms/collection-table.js +5 -4
  18. package/dist/components/cms/collection-table.js.map +1 -1
  19. package/dist/components/cms/detail-editor.js +5 -4
  20. package/dist/components/cms/detail-editor.js.map +1 -1
  21. package/dist/components/cms/editable-cell.js +3 -2
  22. package/dist/components/cms/editable-cell.js.map +1 -1
  23. package/dist/components/cms/field-control.js +6 -5
  24. package/dist/components/cms/field-control.js.map +1 -1
  25. package/dist/components/cms/scalar-value-editor.js +2 -1
  26. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  27. package/dist/components/components/field-inspector.js +3 -2
  28. package/dist/components/components/field-inspector.js.map +1 -1
  29. package/dist/components/current-user-menu.js +4 -2
  30. package/dist/components/current-user-menu.js.map +1 -1
  31. package/dist/components/redirects/redirects-collection-view-source.d.ts +9 -2
  32. package/dist/components/redirects/redirects-collection-view-source.js +51 -8
  33. package/dist/components/redirects/redirects-collection-view-source.js.map +1 -1
  34. package/dist/components/redirects/redirects-module.d.ts +4 -1
  35. package/dist/components/redirects/redirects-module.js +4 -4
  36. package/dist/components/redirects/redirects-module.js.map +1 -1
  37. package/dist/components/redirects/redirects-store.js +5 -1
  38. package/dist/components/redirects/redirects-store.js.map +1 -1
  39. package/dist/customization.d.ts +7 -0
  40. package/dist/customization.js +6 -0
  41. package/dist/customization.js.map +1 -1
  42. package/dist/lib/cms-views/view-model.js +2 -2
  43. package/dist/lib/cms-views/view-model.js.map +1 -1
  44. package/dist/lib/i18n/i18n.d.ts +45 -0
  45. package/dist/lib/i18n/i18n.js +125 -0
  46. package/dist/lib/i18n/i18n.js.map +1 -0
  47. package/dist/lib/i18n/index.d.ts +12 -0
  48. package/dist/lib/i18n/index.js +4 -0
  49. package/dist/lib/i18n/index.js.map +1 -0
  50. package/dist/lib/i18n/messages.en.d.ts +15 -0
  51. package/dist/lib/i18n/messages.en.js +39 -0
  52. package/dist/lib/i18n/messages.en.js.map +1 -0
  53. package/dist/lib/i18n/types.d.ts +45 -0
  54. package/dist/lib/i18n/types.js +19 -0
  55. package/dist/lib/i18n/types.js.map +1 -0
  56. package/dist/lib/persistence/convex-adapter.js +2 -0
  57. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  58. package/dist/lib/references/reference-model.js +4 -3
  59. package/dist/lib/references/reference-model.js.map +1 -1
  60. package/dist/lib/workspace/index.d.ts +8 -0
  61. package/dist/lib/workspace/index.js +10 -0
  62. package/dist/lib/workspace/index.js.map +1 -1
  63. package/dist/ui/badge.d.ts +1 -1
  64. package/package.json +6 -2
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { manifestPropFields } from '@elytracms/core/component-registry';
3
3
  import { Badge, Code, Panel, PanelHeader, Row, Spacer, Text } from '@elytracms/studio/ui';
4
+ import { localizedLabel } from '@elytracms/studio/lib/i18n';
4
5
  /**
5
6
  * The editor control a prop field-def presents (EC-190): derived from the
6
7
  * field `type`, refined by the optional `form.control` hint (textarea / color /
@@ -28,8 +29,8 @@ export function FieldInspector({ manifest }) {
28
29
  return (_jsxs("div", { className: "grid gap-4 lg:grid-cols-2", children: [_jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Props" }), _jsx(Spacer, {}), _jsx(Text, { size: "xs", tone: "muted", mono: true, children: props.length })] }), props.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", className: "p-4", children: "This component declares no editable props." })) : (_jsx("div", { className: "divide-y divide-border", children: props.map((field) => (_jsx(PropRow, { field: field }, field.name))) }))] }), _jsxs(Panel, { children: [_jsxs(PanelHeader, { children: [_jsx(Text, { size: "sm", weight: "semibold", children: "Slots" }), _jsx(Spacer, {}), _jsx(Text, { size: "xs", tone: "muted", mono: true, children: manifest.slots.length })] }), manifest.slots.length === 0 ? (_jsx(Text, { size: "sm", tone: "subtle", className: "p-4", children: "This component has no slots." })) : (_jsx("div", { className: "divide-y divide-border", children: manifest.slots.map((slot) => (_jsx(SlotRow, { slot: slot }, slot.name))) }))] })] }));
29
30
  }
30
31
  function PropRow({ field }) {
31
- const label = field.form?.label;
32
- const description = field.form?.description;
32
+ const label = localizedLabel(field.form?.label, field.name);
33
+ const description = field.form?.description !== undefined ? localizedLabel(field.form.description) : undefined;
33
34
  return (_jsxs("div", { className: "flex flex-col gap-1.5 p-3", children: [_jsxs(Row, { gap: "2", children: [_jsx(Code, { tone: "primary", children: field.name }), _jsx(Badge, { variant: "outline", children: controlType(field) }), _jsx(Spacer, {}), field.form?.inlineEditable ? _jsx(Badge, { variant: "info", children: "inline" }) : null] }), label || description ? (_jsx(Text, { size: "xs", tone: "muted", children: description ?? label })) : null, _jsxs(Row, { gap: "3", className: "text-xs", children: [_jsx(Text, { size: "xs", tone: "subtle", children: "default" }), _jsx(Code, { tone: "muted", children: formatDefault(field.default) })] })] }));
34
35
  }
35
36
  function SlotRow({ slot }) {
@@ -1 +1 @@
1
- {"version":3,"file":"field-inspector.js","sourceRoot":"","sources":["../../../src/components/components/field-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest, SlotSpec } from '@elytracms/core/component-registry'\nimport { manifestPropFields } from '@elytracms/core/component-registry'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport { Badge, Code, Panel, PanelHeader, Row, Spacer, Text } from '@elytracms/studio/ui'\n\n/**\n * The editor control a prop field-def presents (EC-190): derived from the\n * field `type`, refined by the optional `form.control` hint (textarea / color /\n * json for `text`). Read-only display label for the manifest viewer.\n */\nfunction controlType(field: FieldDef): string {\n if (field.type === 'text' && field.form?.control) return field.form.control\n return field.type\n}\n\nfunction formatDefault(value: unknown): string {\n if (value === undefined) return '—'\n if (typeof value === 'string') return value === '' ? '\"\"' : value\n return JSON.stringify(value)\n}\n\n/**\n * Inspect a component's props and slots (brief §7.2, §8.3, EC-053): prop field-def\n * type/control (EC-190), default value, and slot rules (required, allowed\n * children). Props are static field-defs; binding metadata retired in EC-190.\n */\nexport function FieldInspector({ manifest }: { manifest: ComponentManifest }) {\n const props = manifestPropFields(manifest)\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Panel>\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n Props\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"muted\" mono>\n {props.length}\n </Text>\n </PanelHeader>\n {props.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\" className=\"p-4\">\n This component declares no editable props.\n </Text>\n ) : (\n <div className=\"divide-y divide-border\">\n {props.map((field) => (\n <PropRow key={field.name} field={field} />\n ))}\n </div>\n )}\n </Panel>\n\n <Panel>\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n Slots\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"muted\" mono>\n {manifest.slots.length}\n </Text>\n </PanelHeader>\n {manifest.slots.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\" className=\"p-4\">\n This component has no slots.\n </Text>\n ) : (\n <div className=\"divide-y divide-border\">\n {manifest.slots.map((slot) => (\n <SlotRow key={slot.name} slot={slot} />\n ))}\n </div>\n )}\n </Panel>\n </div>\n )\n}\n\nfunction PropRow({ field }: { field: FieldDef }) {\n const label = field.form?.label\n const description = field.form?.description\n return (\n <div className=\"flex flex-col gap-1.5 p-3\">\n <Row gap=\"2\">\n <Code tone=\"primary\">{field.name}</Code>\n <Badge variant=\"outline\">{controlType(field)}</Badge>\n <Spacer />\n {field.form?.inlineEditable ? <Badge variant=\"info\">inline</Badge> : null}\n </Row>\n {label || description ? (\n <Text size=\"xs\" tone=\"muted\">\n {description ?? label}\n </Text>\n ) : null}\n <Row gap=\"3\" className=\"text-xs\">\n <Text size=\"xs\" tone=\"subtle\">\n default\n </Text>\n <Code tone=\"muted\">{formatDefault(field.default)}</Code>\n </Row>\n </div>\n )\n}\n\nfunction SlotRow({ slot }: { slot: SlotSpec }) {\n return (\n <div className=\"flex flex-col gap-1 p-3\">\n <Row gap=\"2\">\n <Code tone=\"primary\">{slot.name}</Code>\n {slot.required ? <Badge variant=\"warning\">required</Badge> : <Badge variant=\"outline\">optional</Badge>}\n <Spacer />\n {slot.label ? (\n <Text size=\"xs\" tone=\"muted\">\n {slot.label}\n </Text>\n ) : null}\n </Row>\n {slot.allow && slot.allow.length > 0 ? (\n <Row gap=\"2\" className=\"text-xs\">\n <Text size=\"xs\" tone=\"subtle\">\n allows\n </Text>\n <Row gap=\"1\" wrap>\n {slot.allow.map((id) => (\n <Code key={id} tone=\"muted\">\n {id}\n </Code>\n ))}\n </Row>\n </Row>\n ) : null}\n </div>\n )\n}\n"]}
1
+ {"version":3,"file":"field-inspector.js","sourceRoot":"","sources":["../../../src/components/components/field-inspector.tsx"],"names":[],"mappings":"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","sourcesContent":["import type { ComponentManifest, SlotSpec } from '@elytracms/core/component-registry'\nimport { manifestPropFields } from '@elytracms/core/component-registry'\nimport type { FieldDef } from '@elytracms/core/cms-core'\nimport { Badge, Code, Panel, PanelHeader, Row, Spacer, Text } from '@elytracms/studio/ui'\nimport { localizedLabel } from '@elytracms/studio/lib/i18n'\n\n/**\n * The editor control a prop field-def presents (EC-190): derived from the\n * field `type`, refined by the optional `form.control` hint (textarea / color /\n * json for `text`). Read-only display label for the manifest viewer.\n */\nfunction controlType(field: FieldDef): string {\n if (field.type === 'text' && field.form?.control) return field.form.control\n return field.type\n}\n\nfunction formatDefault(value: unknown): string {\n if (value === undefined) return '—'\n if (typeof value === 'string') return value === '' ? '\"\"' : value\n return JSON.stringify(value)\n}\n\n/**\n * Inspect a component's props and slots (brief §7.2, §8.3, EC-053): prop field-def\n * type/control (EC-190), default value, and slot rules (required, allowed\n * children). Props are static field-defs; binding metadata retired in EC-190.\n */\nexport function FieldInspector({ manifest }: { manifest: ComponentManifest }) {\n const props = manifestPropFields(manifest)\n return (\n <div className=\"grid gap-4 lg:grid-cols-2\">\n <Panel>\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n Props\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"muted\" mono>\n {props.length}\n </Text>\n </PanelHeader>\n {props.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\" className=\"p-4\">\n This component declares no editable props.\n </Text>\n ) : (\n <div className=\"divide-y divide-border\">\n {props.map((field) => (\n <PropRow key={field.name} field={field} />\n ))}\n </div>\n )}\n </Panel>\n\n <Panel>\n <PanelHeader>\n <Text size=\"sm\" weight=\"semibold\">\n Slots\n </Text>\n <Spacer />\n <Text size=\"xs\" tone=\"muted\" mono>\n {manifest.slots.length}\n </Text>\n </PanelHeader>\n {manifest.slots.length === 0 ? (\n <Text size=\"sm\" tone=\"subtle\" className=\"p-4\">\n This component has no slots.\n </Text>\n ) : (\n <div className=\"divide-y divide-border\">\n {manifest.slots.map((slot) => (\n <SlotRow key={slot.name} slot={slot} />\n ))}\n </div>\n )}\n </Panel>\n </div>\n )\n}\n\nfunction PropRow({ field }: { field: FieldDef }) {\n const label = localizedLabel(field.form?.label, field.name)\n const description = field.form?.description !== undefined ? localizedLabel(field.form.description) : undefined\n return (\n <div className=\"flex flex-col gap-1.5 p-3\">\n <Row gap=\"2\">\n <Code tone=\"primary\">{field.name}</Code>\n <Badge variant=\"outline\">{controlType(field)}</Badge>\n <Spacer />\n {field.form?.inlineEditable ? <Badge variant=\"info\">inline</Badge> : null}\n </Row>\n {label || description ? (\n <Text size=\"xs\" tone=\"muted\">\n {description ?? label}\n </Text>\n ) : null}\n <Row gap=\"3\" className=\"text-xs\">\n <Text size=\"xs\" tone=\"subtle\">\n default\n </Text>\n <Code tone=\"muted\">{formatDefault(field.default)}</Code>\n </Row>\n </div>\n )\n}\n\nfunction SlotRow({ slot }: { slot: SlotSpec }) {\n return (\n <div className=\"flex flex-col gap-1 p-3\">\n <Row gap=\"2\">\n <Code tone=\"primary\">{slot.name}</Code>\n {slot.required ? <Badge variant=\"warning\">required</Badge> : <Badge variant=\"outline\">optional</Badge>}\n <Spacer />\n {slot.label ? (\n <Text size=\"xs\" tone=\"muted\">\n {slot.label}\n </Text>\n ) : null}\n </Row>\n {slot.allow && slot.allow.length > 0 ? (\n <Row gap=\"2\" className=\"text-xs\">\n <Text size=\"xs\" tone=\"subtle\">\n allows\n </Text>\n <Row gap=\"1\" wrap>\n {slot.allow.map((id) => (\n <Code key={id} tone=\"muted\">\n {id}\n </Code>\n ))}\n </Row>\n </Row>\n ) : null}\n </div>\n )\n}\n"]}
@@ -2,6 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { LogOut, Moon, Settings, Sun } from 'lucide-react';
3
3
  import { roleGrantsCapability } from '@elytracms/core/persistence';
4
4
  import { Avatar, Badge, Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, Row, Spacer, Stack, Text, } from '@elytracms/studio/ui';
5
+ import { useT } from '@elytracms/studio/lib/i18n';
5
6
  import { useAuth, useProjectRole } from './auth-context';
6
7
  import { useTheme } from './theme-context';
7
8
  /**
@@ -14,16 +15,17 @@ export function CurrentUserMenu({ projectId }) {
14
15
  const auth = useAuth();
15
16
  const role = useProjectRole(projectId);
16
17
  const { theme, setTheme } = useTheme();
18
+ const t = useT();
17
19
  const user = auth.session?.user;
18
20
  if (!user)
19
21
  return null;
20
22
  // Show one identity line — the name if set, otherwise the email (never both).
21
23
  const displayName = user.name ?? user.email;
22
24
  const isDark = theme === 'dark';
23
- return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", className: "rounded-full", "aria-label": "Account menu", children: _jsx(Avatar, { label: displayName, size: "sm" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-60", children: [_jsxs(Row, { gap: "2", className: "px-2 py-2", children: [_jsx(Avatar, { label: displayName, size: "md" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate leading-tight", children: displayName }), role ? (_jsx(Row, { children: _jsx(RoleBadge, { role: role }) })) : null] })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: (event) => {
25
+ return (_jsxs(DropdownMenu, { children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon-sm", className: "rounded-full", "aria-label": t('userMenu.accountMenu'), children: _jsx(Avatar, { label: displayName, size: "sm" }) }) }), _jsxs(DropdownMenuContent, { align: "end", className: "min-w-60", children: [_jsxs(Row, { gap: "2", className: "px-2 py-2", children: [_jsx(Avatar, { label: displayName, size: "md" }), _jsxs(Stack, { gap: "1", className: "min-w-0 flex-1", children: [_jsx(Text, { size: "sm", weight: "medium", className: "truncate leading-tight", children: displayName }), role ? (_jsx(Row, { children: _jsx(RoleBadge, { role: role }) })) : null] })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { onSelect: (event) => {
24
26
  event.preventDefault();
25
27
  setTheme(isDark ? 'light' : 'dark');
26
- }, children: [isDark ? _jsx(Sun, {}) : _jsx(Moon, {}), isDark ? 'Light mode' : 'Dark mode'] }), _jsxs(DropdownMenuItem, { disabled: true, children: [_jsx(Settings, {}), "Account settings", _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: "Soon" })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onSelect: () => void auth.signOut(), children: [_jsx(LogOut, {}), "Sign out"] })] })] }));
28
+ }, children: [isDark ? _jsx(Sun, {}) : _jsx(Moon, {}), isDark ? t('userMenu.lightMode') : t('userMenu.darkMode')] }), _jsxs(DropdownMenuItem, { disabled: true, children: [_jsx(Settings, {}), t('userMenu.accountSettings'), _jsx(Spacer, {}), _jsx(Badge, { variant: "outline", children: t('userMenu.soon') })] }), _jsx(DropdownMenuSeparator, {}), _jsxs(DropdownMenuItem, { variant: "destructive", onSelect: () => void auth.signOut(), children: [_jsx(LogOut, {}), t('userMenu.signOut')] })] })] }));
27
29
  }
28
30
  export function RoleBadge({ role }) {
29
31
  // Variant by capability tier (EC-240c), not role literals — a custom role set
@@ -1 +1 @@
1
- {"version":3,"file":"current-user-menu.js","sourceRoot":"","sources":["../../src/components/current-user-menu.tsx"],"names":[],"mappings":"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","sourcesContent":["import { LogOut, Moon, Settings, Sun } from 'lucide-react'\nimport { roleGrantsCapability } from '@elytracms/core/persistence'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport {\n Avatar,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useAuth, useProjectRole } from './auth-context'\nimport { useTheme } from './theme-context'\n\n/**\n * Current-user affordance in the shell header (EC-150): who is signed in,\n * their role on the active project (badge), theme switch, and sign-out. Role\n * display is informational — actual enforcement happens at the operations\n * chokepoint and server-side, never in the UI.\n */\nexport function CurrentUserMenu({ projectId }: { projectId: string | null }) {\n const auth = useAuth()\n const role = useProjectRole(projectId)\n const { theme, setTheme } = useTheme()\n const user = auth.session?.user\n if (!user) return null\n\n // Show one identity line — the name if set, otherwise the email (never both).\n const displayName = user.name ?? user.email\n const isDark = theme === 'dark'\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" className=\"rounded-full\" aria-label=\"Account menu\">\n <Avatar label={displayName} size=\"sm\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-60\">\n {/* Identity header — normal case, single line, with the project role. */}\n <Row gap=\"2\" className=\"px-2 py-2\">\n <Avatar label={displayName} size=\"md\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate leading-tight\">\n {displayName}\n </Text>\n {role ? (\n <Row>\n <RoleBadge role={role} />\n </Row>\n ) : null}\n </Stack>\n </Row>\n <DropdownMenuSeparator />\n {/* Theme switch — preventDefault keeps the menu open so the change is visible. */}\n <DropdownMenuItem\n onSelect={(event) => {\n event.preventDefault()\n setTheme(isDark ? 'light' : 'dark')\n }}\n >\n {isDark ? <Sun /> : <Moon />}\n {isDark ? 'Light mode' : 'Dark mode'}\n </DropdownMenuItem>\n {/* TODO(user-settings): point this at an account-settings surface once one\n exists (today only project-scoped settings are implemented). */}\n <DropdownMenuItem disabled>\n <Settings />\n Account settings\n <Spacer />\n <Badge variant=\"outline\">Soon</Badge>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onSelect={() => void auth.signOut()}>\n <LogOut />\n Sign out\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nexport function RoleBadge({ role }: { role: ProjectRole }) {\n // Variant by capability tier (EC-240c), not role literals — a custom role set\n // styles by what it grants: administer → primary, write → info, else outline.\n const variant = roleGrantsCapability(role, 'project.administer')\n ? 'primary'\n : roleGrantsCapability(role, 'content.write')\n ? 'info'\n : 'outline'\n return <Badge variant={variant}>{role}</Badge>\n}\n"]}
1
+ {"version":3,"file":"current-user-menu.js","sourceRoot":"","sources":["../../src/components/current-user-menu.tsx"],"names":[],"mappings":"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","sourcesContent":["import { LogOut, Moon, Settings, Sun } from 'lucide-react'\nimport { roleGrantsCapability } from '@elytracms/core/persistence'\nimport type { ProjectRole } from '@elytracms/core/persistence'\nimport {\n Avatar,\n Badge,\n Button,\n DropdownMenu,\n DropdownMenuContent,\n DropdownMenuItem,\n DropdownMenuSeparator,\n DropdownMenuTrigger,\n Row,\n Spacer,\n Stack,\n Text,\n} from '@elytracms/studio/ui'\nimport { useT } from '@elytracms/studio/lib/i18n'\nimport { useAuth, useProjectRole } from './auth-context'\nimport { useTheme } from './theme-context'\n\n/**\n * Current-user affordance in the shell header (EC-150): who is signed in,\n * their role on the active project (badge), theme switch, and sign-out. Role\n * display is informational — actual enforcement happens at the operations\n * chokepoint and server-side, never in the UI.\n */\nexport function CurrentUserMenu({ projectId }: { projectId: string | null }) {\n const auth = useAuth()\n const role = useProjectRole(projectId)\n const { theme, setTheme } = useTheme()\n const t = useT()\n const user = auth.session?.user\n if (!user) return null\n\n // Show one identity line — the name if set, otherwise the email (never both).\n const displayName = user.name ?? user.email\n const isDark = theme === 'dark'\n\n return (\n <DropdownMenu>\n <DropdownMenuTrigger asChild>\n <Button variant=\"ghost\" size=\"icon-sm\" className=\"rounded-full\" aria-label={t('userMenu.accountMenu')}>\n <Avatar label={displayName} size=\"sm\" />\n </Button>\n </DropdownMenuTrigger>\n <DropdownMenuContent align=\"end\" className=\"min-w-60\">\n {/* Identity header — normal case, single line, with the project role. */}\n <Row gap=\"2\" className=\"px-2 py-2\">\n <Avatar label={displayName} size=\"md\" />\n <Stack gap=\"1\" className=\"min-w-0 flex-1\">\n <Text size=\"sm\" weight=\"medium\" className=\"truncate leading-tight\">\n {displayName}\n </Text>\n {role ? (\n <Row>\n <RoleBadge role={role} />\n </Row>\n ) : null}\n </Stack>\n </Row>\n <DropdownMenuSeparator />\n {/* Theme switch — preventDefault keeps the menu open so the change is visible. */}\n <DropdownMenuItem\n onSelect={(event) => {\n event.preventDefault()\n setTheme(isDark ? 'light' : 'dark')\n }}\n >\n {isDark ? <Sun /> : <Moon />}\n {isDark ? t('userMenu.lightMode') : t('userMenu.darkMode')}\n </DropdownMenuItem>\n {/* TODO(user-settings): point this at an account-settings surface once one\n exists (today only project-scoped settings are implemented). */}\n <DropdownMenuItem disabled>\n <Settings />\n {t('userMenu.accountSettings')}\n <Spacer />\n <Badge variant=\"outline\">{t('userMenu.soon')}</Badge>\n </DropdownMenuItem>\n <DropdownMenuSeparator />\n <DropdownMenuItem variant=\"destructive\" onSelect={() => void auth.signOut()}>\n <LogOut />\n {t('userMenu.signOut')}\n </DropdownMenuItem>\n </DropdownMenuContent>\n </DropdownMenu>\n )\n}\n\nexport function RoleBadge({ role }: { role: ProjectRole }) {\n // Variant by capability tier (EC-240c), not role literals — a custom role set\n // styles by what it grants: administer → primary, write → info, else outline.\n const variant = roleGrantsCapability(role, 'project.administer')\n ? 'primary'\n : roleGrantsCapability(role, 'content.write')\n ? 'info'\n : 'outline'\n return <Badge variant={variant}>{role}</Badge>\n}\n"]}
@@ -1,4 +1,4 @@
1
- import type { CmsDocument } from '@elytracms/core/cms-core';
1
+ import type { CmsDocument, CollectionDef } from '@elytracms/core/cms-core';
2
2
  import type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence';
3
3
  import type { CollectionViewSource } from '@elytracms/studio/lib/collection-view/collection-view-source';
4
4
  /** Project a stored redirect into the lens's `values` shape. */
@@ -14,5 +14,12 @@ export declare function documentToSaveRedirect(doc: CmsDocument): SaveRedirectIn
14
14
  * is a small record saved explicitly (a saved+enabled one is live).
15
15
  *
16
16
  * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).
17
+ *
18
+ * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may
19
+ * pass a customized def (EC-311 — built-in fields overridden by equal name via
20
+ * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`
21
+ * names+types are contract-locked, so the projection helpers still read the same
22
+ * canonical values; a mount's *additive* fields (Tier 2) round-trip through the
23
+ * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).
17
24
  */
18
- export declare function useRedirectsCollectionViewSource(projectId: string): CollectionViewSource;
25
+ export declare function useRedirectsCollectionViewSource(projectId: string, schema?: CollectionDef): CollectionViewSource;
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useMemo, useState } from 'react';
2
+ import { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core';
2
3
  import { studioPath } from '@elytracms/studio/router';
3
4
  import { defaultViewState } from '@elytracms/studio/lib/cms-views';
4
5
  import { useRedirects } from './redirects-store';
@@ -9,6 +10,13 @@ const REDIRECTS_LOCALE_CONFIG = {
9
10
  default: REDIRECTS_LOCALE,
10
11
  locales: [REDIRECTS_LOCALE],
11
12
  };
13
+ /**
14
+ * The canonical redirect fields — the storage/delivery contract of the primitive
15
+ * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311
16
+ * Tier 2) is by definition NOT one of these, so its value round-trips through the
17
+ * record's open `meta` bag. Derived from the built-in def so the two never drift.
18
+ */
19
+ const CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name));
12
20
  /** Project a stored redirect into the lens's `values` shape. */
13
21
  export function redirectToDocument(entry) {
14
22
  return {
@@ -19,18 +27,30 @@ export function redirectToDocument(entry) {
19
27
  to: entry.to,
20
28
  permanent: entry.permanent,
21
29
  enabled: entry.enabled,
30
+ // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.
31
+ ...(entry.meta ?? {}),
22
32
  },
23
33
  };
24
34
  }
25
35
  /** Map an edited lens document back to the redirect write contract. */
26
36
  export function documentToSaveRedirect(doc) {
27
37
  const values = doc.values ?? {};
38
+ // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's
39
+ // additive `nav.redirects({ fields })` override — persist it in the meta bag. The
40
+ // editor holds the complete additive set, so this is the whole bag (replace, not
41
+ // merge). Omit `meta` entirely for the default rail so its saves are unchanged.
42
+ const meta = {};
43
+ for (const [key, value] of Object.entries(values)) {
44
+ if (!CANONICAL_REDIRECT_FIELDS.has(key))
45
+ meta[key] = value;
46
+ }
28
47
  return {
29
48
  id: doc.id,
30
49
  from: typeof values.from === 'string' ? values.from : '',
31
50
  to: typeof values.to === 'string' ? values.to : '',
32
51
  permanent: typeof values.permanent === 'boolean' ? values.permanent : true,
33
- enabled: typeof values.enabled === 'boolean' ? values.enabled : false,
52
+ enabled: typeof values.enabled === 'boolean' ? values.enabled : true,
53
+ ...(Object.keys(meta).length > 0 ? { meta } : {}),
34
54
  };
35
55
  }
36
56
  /**
@@ -42,11 +62,21 @@ export function documentToSaveRedirect(doc) {
42
62
  * is a small record saved explicitly (a saved+enabled one is live).
43
63
  *
44
64
  * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).
65
+ *
66
+ * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may
67
+ * pass a customized def (EC-311 — built-in fields overridden by equal name via
68
+ * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`
69
+ * names+types are contract-locked, so the projection helpers still read the same
70
+ * canonical values; a mount's *additive* fields (Tier 2) round-trip through the
71
+ * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).
45
72
  */
46
- export function useRedirectsCollectionViewSource(projectId) {
73
+ export function useRedirectsCollectionViewSource(projectId, schema = redirectsCollectionDef) {
47
74
  const store = useRedirects();
48
- const [viewState, setViewStateRaw] = useState(() => defaultViewState(redirectsCollectionDef));
75
+ const [viewState, setViewStateRaw] = useState(() => defaultViewState(schema));
49
76
  const [lastError, setLastError] = useState(null);
77
+ // EC-311: run the (possibly customized) schema's validation — required/pattern and
78
+ // custom `validate` fns on an overridden field must actually fire in the editor.
79
+ const registry = useMemo(() => createCollectionRegistry([schema]), [schema]);
50
80
  const documents = useCallback(() => store.redirects.map(redirectToDocument), [store.redirects]);
51
81
  const get = useCallback((ref) => {
52
82
  const record = store.get(ref.id);
@@ -84,8 +114,8 @@ export function useRedirectsCollectionViewSource(projectId) {
84
114
  adminOnly: false, // content, not administration
85
115
  };
86
116
  const source = {
87
- collectionId: redirectsCollectionDef.id,
88
- schema: redirectsCollectionDef,
117
+ collectionId: schema.id,
118
+ schema,
89
119
  capabilities,
90
120
  projectId,
91
121
  localeConfig: REDIRECTS_LOCALE_CONFIG,
@@ -95,11 +125,11 @@ export function useRedirectsCollectionViewSource(projectId) {
95
125
  titleOf,
96
126
  // Redirects carry no relation/asset fields, so nothing cross-collection to resolve.
97
127
  referenceCandidates: () => [],
98
- collections: [redirectsCollectionDef],
128
+ collections: [schema],
99
129
  detailPath: (id) => studioPath.redirect(id),
100
130
  listPath: studioPath.redirects(),
101
131
  isFieldReadOnly: (field) => field.form?.readOnly ?? false,
102
- validate: () => [],
132
+ validate: (doc) => validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),
103
133
  lastError,
104
134
  clearLastError: () => setLastError(null),
105
135
  viewState: () => viewState,
@@ -109,6 +139,19 @@ export function useRedirectsCollectionViewSource(projectId) {
109
139
  remove,
110
140
  };
111
141
  return source;
112
- }, [projectId, documents, get, titleOf, setViewState, create, save, remove, viewState, lastError]);
142
+ }, [
143
+ projectId,
144
+ schema,
145
+ registry,
146
+ documents,
147
+ get,
148
+ titleOf,
149
+ setViewState,
150
+ create,
151
+ save,
152
+ remove,
153
+ viewState,
154
+ lastError,
155
+ ]);
113
156
  }
114
157
  //# sourceMappingURL=redirects-collection-view-source.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAUtD,OAAO,EAAE,UAAU,EAAE,MAAM,0BAA0B,CAAA;AACrD,OAAO,EAAE,gBAAgB,EAAE,MAAM,iCAAiC,CAAA;AAMlE,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AAChD,OAAO,EAAE,sBAAsB,EAAE,MAAM,oBAAoB,CAAA;AAE3D,uFAAuF;AACvF,MAAM,gBAAgB,GAAW,IAAI,CAAA;AACrC,MAAM,uBAAuB,GAAiB;IAC5C,OAAO,EAAE,gBAAgB;IACzB,OAAO,EAAE,CAAC,gBAAgB,CAAC;CAC5B,CAAA;AAED,gEAAgE;AAChE,MAAM,UAAU,kBAAkB,CAAC,KAAoB;IACrD,OAAO;QACL,UAAU,EAAE,sBAAsB,CAAC,EAAE;QACrC,EAAE,EAAE,KAAK,CAAC,EAAE;QACZ,MAAM,EAAE;YACN,IAAI,EAAE,KAAK,CAAC,IAAI;YAChB,EAAE,EAAE,KAAK,CAAC,EAAE;YACZ,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,OAAO,EAAE,KAAK,CAAC,OAAO;SACvB;KACF,CAAA;AACH,CAAC;AAED,uEAAuE;AACvE,MAAM,UAAU,sBAAsB,CAAC,GAAgB;IACrD,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,EAAE,CAAA;IAC/B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,IAAI,EAAE,OAAO,MAAM,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QACxD,EAAE,EAAE,OAAO,MAAM,CAAC,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE;QAClD,SAAS,EAAE,OAAO,MAAM,CAAC,SAAS,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI;QAC1E,OAAO,EAAE,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK;KACtE,CAAA;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gCAAgC,CAAC,SAAiB;IAChE,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,CAAC,SAAS,EAAE,eAAe,CAAC,GAAG,QAAQ,CAAiB,GAAG,EAAE,CACjE,gBAAgB,CAAC,sBAAsB,CAAC,CACzC,CAAA;IACD,MAAM,CAAC,SAAS,EAAE,YAAY,CAAC,GAAG,QAAQ,CAAwB,IAAI,CAAC,CAAA;IAEvE,MAAM,SAAS,GAAG,WAAW,CAC3B,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,GAAG,CAAC,kBAAkB,CAAC,EAC7C,CAAC,KAAK,CAAC,SAAS,CAAC,CAClB,CAAA;IACD,MAAM,GAAG,GAAG,WAAW,CACrB,CAAC,GAAgB,EAA2B,EAAE;QAC5C,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QAChC,OAAO,MAAM,CAAC,CAAC,CAAC,kBAAkB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACxD,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAA;IACD,MAAM,OAAO,GAAG,WAAW,CACzB,CAAC,GAAgB,EAAU,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,IAAI,IAAI,gBAAgB,EACzE,CAAC,KAAK,CAAC,CACR,CAAA;IACD,MAAM,YAAY,GAAG,WAAW,CAAuC,CAAC,MAAM,EAAE,EAAE;QAChF,eAAe,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,OAAO,MAAM,KAAK,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAA;IACnF,CAAC,EAAE,EAAE,CAAC,CAAA;IAEN,MAAM,MAAM,GAAG,WAAW,CACxB,GAAgB,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,sBAAsB,CAAC,EAAE,EAAE,EAAE,EAAE,KAAK,CAAC,WAAW,EAAE,EAAE,CAAC,EACvF,CAAC,KAAK,CAAC,CACR,CAAA;IACD,MAAM,IAAI,GAAG,WAAW,CACtB,CAAC,GAAgB,EAAE,EAAE;QACnB,KAAK,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YAC3D,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;QAC5E,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAA;IACD,MAAM,MAAM,GAAG,WAAW,CACxB,CAAC,GAAgB,EAAE,EAAE;QACnB,KAAK,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACxC,IAAI,CAAC,MAAM,CAAC,EAAE;gBAAE,YAAY,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,CAAC,CAAA;QAC5E,CAAC,CAAC,CAAA;IACJ,CAAC,EACD,CAAC,KAAK,CAAC,CACR,CAAA;IAED,OAAO,OAAO,CAAC,GAAG,EAAE;QAClB,MAAM,YAAY,GAA+B;YAC/C,OAAO,EAAE,KAAK;YACd,QAAQ,EAAE,KAAK;YACf,OAAO,EAAE,KAAK;YACd,UAAU,EAAE,KAAK;YACjB,MAAM,EAAE,IAAI,EAAE,2BAA2B;YACzC,MAAM,EAAE,IAAI;YACZ,IAAI,EAAE,KAAK;YACX,OAAO,EAAE,KAAK;YACd,SAAS,EAAE,KAAK;YAChB,YAAY,EAAE,KAAK;YACnB,SAAS,EAAE,KAAK,EAAE,8BAA8B;SACjD,CAAA;QACD,MAAM,MAAM,GAAyB;YACnC,YAAY,EAAE,sBAAsB,CAAC,EAAE;YACvC,MAAM,EAAE,sBAAsB;YAC9B,YAAY;YACZ,SAAS;YACT,YAAY,EAAE,uBAAuB;YACrC,YAAY,EAAE,gBAAgB;YAC9B,SAAS;YACT,GAAG;YACH,OAAO;YACP,oFAAoF;YACpF,mBAAmB,EAAE,GAAG,EAAE,CAAC,EAAE;YAC7B,WAAW,EAAE,CAAC,sBAAsB,CAAC;YACrC,UAAU,EAAE,CAAC,EAAU,EAAE,EAAE,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;YACnD,QAAQ,EAAE,UAAU,CAAC,SAAS,EAAE;YAChC,eAAe,EAAE,CAAC,KAAe,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,QAAQ,IAAI,KAAK;YACnE,QAAQ,EAAE,GAAG,EAAE,CAAC,EAAE;YAClB,SAAS;YACT,cAAc,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC;YACxC,SAAS,EAAE,GAAG,EAAE,CAAC,SAAS;YAC1B,YAAY;YACZ,MAAM;YACN,IAAI;YACJ,MAAM;SACP,CAAA;QACD,OAAO,MAAM,CAAA;IACf,CAAC,EAAE,CAAC,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC,CAAA;AACpG,CAAC","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : false,\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n */\nexport function useRedirectsCollectionViewSource(projectId: string): CollectionViewSource {\n const store = useRedirects()\n const [viewState, setViewStateRaw] = useState<TableViewState>(() =>\n defaultViewState(redirectsCollectionDef),\n )\n const [lastError, setLastError] = useState<OperationError | null>(null)\n\n const documents = useCallback(\n () => store.redirects.map(redirectToDocument),\n [store.redirects],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback(\n (doc: CmsDocument) => {\n void store.save(documentToSaveRedirect(doc)).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n const remove = useCallback(\n (ref: DocumentRef) => {\n void store.remove(ref.id).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n\n return useMemo(() => {\n const capabilities: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n }\n const source: CollectionViewSource = {\n collectionId: redirectsCollectionDef.id,\n schema: redirectsCollectionDef,\n capabilities,\n projectId,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n activeLocale: REDIRECTS_LOCALE,\n documents,\n get,\n titleOf,\n // Redirects carry no relation/asset fields, so nothing cross-collection to resolve.\n referenceCandidates: () => [],\n collections: [redirectsCollectionDef],\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n isFieldReadOnly: (field: FieldDef) => field.form?.readOnly ?? false,\n validate: () => [],\n lastError,\n clearLastError: () => setLastError(null),\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n }\n return source\n }, [projectId, documents, get, titleOf, setViewState, create, save, remove, viewState, lastError])\n}\n"]}
1
+ {"version":3,"file":"redirects-collection-view-source.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-collection-view-source.ts"],"names":[],"mappings":"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","sourcesContent":["import { useCallback, useMemo, useState } from 'react'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n FieldDef,\n Locale,\n LocaleConfig,\n} from '@elytracms/core/cms-core'\nimport { createCollectionRegistry, validateDocument } from '@elytracms/core/cms-core'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport type { OperationError } from '@elytracms/core/operations'\nimport { studioPath } from '@elytracms/studio/router'\nimport { defaultViewState } from '@elytracms/studio/lib/cms-views'\nimport type { TableViewState } from '@elytracms/studio/lib/cms-views'\nimport type {\n CollectionViewCapabilities,\n CollectionViewSource,\n} from '@elytracms/studio/lib/collection-view/collection-view-source'\nimport { useRedirects } from './redirects-store'\nimport { redirectsCollectionDef } from './redirects-schema'\n\n/** The redirects rail is not localized — one implicit locale keeps the lens honest. */\nconst REDIRECTS_LOCALE: Locale = 'en'\nconst REDIRECTS_LOCALE_CONFIG: LocaleConfig = {\n default: REDIRECTS_LOCALE,\n locales: [REDIRECTS_LOCALE],\n}\n\n/**\n * The canonical redirect fields — the storage/delivery contract of the primitive\n * rail. Anything a mount adds via *additive* `nav.redirects({ fields })` (EC-311\n * Tier 2) is by definition NOT one of these, so its value round-trips through the\n * record's open `meta` bag. Derived from the built-in def so the two never drift.\n */\nconst CANONICAL_REDIRECT_FIELDS = new Set(redirectsCollectionDef.fields.map((f) => f.name))\n\n/** Project a stored redirect into the lens's `values` shape. */\nexport function redirectToDocument(entry: RedirectEntry): CmsDocument {\n return {\n collection: redirectsCollectionDef.id,\n id: entry.id,\n values: {\n from: entry.from,\n to: entry.to,\n permanent: entry.permanent,\n enabled: entry.enabled,\n // EC-311 Tier 2: additive schema fields round-trip through the open meta bag.\n ...(entry.meta ?? {}),\n },\n }\n}\n\n/** Map an edited lens document back to the redirect write contract. */\nexport function documentToSaveRedirect(doc: CmsDocument): SaveRedirectInput {\n const values = doc.values ?? {}\n // EC-311 Tier 2: any value keyed outside the canonical set comes from a mount's\n // additive `nav.redirects({ fields })` override — persist it in the meta bag. The\n // editor holds the complete additive set, so this is the whole bag (replace, not\n // merge). Omit `meta` entirely for the default rail so its saves are unchanged.\n const meta: Record<string, unknown> = {}\n for (const [key, value] of Object.entries(values)) {\n if (!CANONICAL_REDIRECT_FIELDS.has(key)) meta[key] = value\n }\n return {\n id: doc.id,\n from: typeof values.from === 'string' ? values.from : '',\n to: typeof values.to === 'string' ? values.to : '',\n permanent: typeof values.permanent === 'boolean' ? values.permanent : true,\n enabled: typeof values.enabled === 'boolean' ? values.enabled : true,\n ...(Object.keys(meta).length > 0 ? { meta } : {}),\n }\n}\n\n/**\n * Assemble a {@link CollectionViewSource} for the **redirects rail** (EC-303) —\n * the portable list/edit surfaces over the editorial redirects store instead of\n * the CMS store. Redirects are CONTENT (an editor owns them), so unlike the users\n * rail this source allows `create`, is NOT admin-only, and needs no principal\n * field-access policy. No publish/versions/locales/inline/bulk/import — a redirect\n * is a small record saved explicitly (a saved+enabled one is live).\n *\n * Must be mounted under a `RedirectsProvider` (for {@link useRedirects}).\n *\n * `schema` defaults to the engine-owned {@link redirectsCollectionDef}; a mount may\n * pass a customized def (EC-311 — built-in fields overridden by equal name via\n * `nav.redirects({ fields })`). The canonical `from`/`to`/`permanent`/`enabled`\n * names+types are contract-locked, so the projection helpers still read the same\n * canonical values; a mount's *additive* fields (Tier 2) round-trip through the\n * record's open `meta` bag ({@link documentToSaveRedirect}/{@link redirectToDocument}).\n */\nexport function useRedirectsCollectionViewSource(\n projectId: string,\n schema: CollectionDef = redirectsCollectionDef,\n): CollectionViewSource {\n const store = useRedirects()\n const [viewState, setViewStateRaw] = useState<TableViewState>(() => defaultViewState(schema))\n const [lastError, setLastError] = useState<OperationError | null>(null)\n // EC-311: run the (possibly customized) schema's validation — required/pattern and\n // custom `validate` fns on an overridden field must actually fire in the editor.\n const registry = useMemo(() => createCollectionRegistry([schema]), [schema])\n\n const documents = useCallback(\n () => store.redirects.map(redirectToDocument),\n [store.redirects],\n )\n const get = useCallback(\n (ref: DocumentRef): CmsDocument | undefined => {\n const record = store.get(ref.id)\n return record ? redirectToDocument(record) : undefined\n },\n [store],\n )\n const titleOf = useCallback(\n (ref: DocumentRef): string => store.get(ref.id)?.from || '(new redirect)',\n [store],\n )\n const setViewState = useCallback<CollectionViewSource['setViewState']>((update) => {\n setViewStateRaw((prev) => (typeof update === 'function' ? update(prev) : update))\n }, [])\n\n const create = useCallback(\n (): DocumentRef => ({ collection: redirectsCollectionDef.id, id: store.createDraft() }),\n [store],\n )\n const save = useCallback(\n (doc: CmsDocument) => {\n void store.save(documentToSaveRedirect(doc)).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n const remove = useCallback(\n (ref: DocumentRef) => {\n void store.remove(ref.id).then((result) => {\n if (!result.ok) setLastError({ code: 'unknown', message: result.message })\n })\n },\n [store],\n )\n\n return useMemo(() => {\n const capabilities: CollectionViewCapabilities = {\n publish: false,\n versions: false,\n locales: false,\n inlineEdit: false,\n create: true, // editors create redirects\n remove: true,\n bulk: false,\n liveUrl: false,\n hierarchy: false,\n importExport: false,\n adminOnly: false, // content, not administration\n }\n const source: CollectionViewSource = {\n collectionId: schema.id,\n schema,\n capabilities,\n projectId,\n localeConfig: REDIRECTS_LOCALE_CONFIG,\n activeLocale: REDIRECTS_LOCALE,\n documents,\n get,\n titleOf,\n // Redirects carry no relation/asset fields, so nothing cross-collection to resolve.\n referenceCandidates: () => [],\n collections: [schema],\n detailPath: (id: string) => studioPath.redirect(id),\n listPath: studioPath.redirects(),\n isFieldReadOnly: (field: FieldDef) => field.form?.readOnly ?? false,\n validate: (doc: CmsDocument) =>\n validateDocument(registry, doc, { localeConfig: REDIRECTS_LOCALE_CONFIG }),\n lastError,\n clearLastError: () => setLastError(null),\n viewState: () => viewState,\n setViewState,\n create,\n save,\n remove,\n }\n return source\n }, [\n projectId,\n schema,\n registry,\n documents,\n get,\n titleOf,\n setViewState,\n create,\n save,\n remove,\n viewState,\n lastError,\n ])\n}\n"]}
@@ -1,3 +1,4 @@
1
+ import type { CollectionDef } from '@elytracms/core/cms-core';
1
2
  /**
2
3
  * The studio Redirects module (EC-303). Editorial redirects as data — the reuse
3
4
  * proof (with the users rail) that the portable interaction surfaces serve any
@@ -12,7 +13,9 @@
12
13
  * (routed via `studioPath.redirect`). No admin gate — redirects are content, so
13
14
  * viewers read and editors write (enforced at the chokepoint + Convex boundary).
14
15
  */
15
- export declare function RedirectsModule({ projectId, redirectId, }: {
16
+ export declare function RedirectsModule({ projectId, redirectId, schema, }: {
16
17
  projectId: string;
17
18
  redirectId?: string | null;
19
+ /** EC-311: a mount-customized redirects def (built-in fields overridden by name). */
20
+ schema?: CollectionDef;
18
21
  }): import("react").JSX.Element;
@@ -20,13 +20,13 @@ import { PortableDetailEditor } from '../cms/detail-editor';
20
20
  * (routed via `studioPath.redirect`). No admin gate — redirects are content, so
21
21
  * viewers read and editors write (enforced at the chokepoint + Convex boundary).
22
22
  */
23
- export function RedirectsModule({ projectId, redirectId = null, }) {
24
- return (_jsx(RedirectsProvider, { projectId: projectId, children: _jsx(RedirectsSurface, { projectId: projectId, redirectId: redirectId }) }));
23
+ export function RedirectsModule({ projectId, redirectId = null, schema, }) {
24
+ return (_jsx(RedirectsProvider, { projectId: projectId, children: _jsx(RedirectsSurface, { projectId: projectId, redirectId: redirectId, schema: schema }) }));
25
25
  }
26
26
  /** The loaded surface: the portable list (or one redirect's detail) over the source. */
27
- function RedirectsSurface({ projectId, redirectId, }) {
27
+ function RedirectsSurface({ projectId, redirectId, schema, }) {
28
28
  const store = useRedirects();
29
- const source = useRedirectsCollectionViewSource(projectId);
29
+ const source = useRedirectsCollectionViewSource(projectId, schema);
30
30
  if (store.status === 'error') {
31
31
  return (_jsxs(Stack, { gap: "2", className: "p-6", "data-testid": "redirects-error", children: [_jsxs(Row, { gap: "2", children: [_jsx(AlertTriangle, { className: "size-4 text-destructive" }), _jsx(Text, { weight: "semibold", children: "Redirects failed to load" })] }), _jsx(Text, { size: "sm", tone: "muted", children: store.errorMessage ?? 'The persistence backend is unreachable.' }), _jsx(Row, { children: _jsxs(Button, { variant: "outline", size: "sm", onClick: () => void store.refresh(), children: [_jsx(RotateCcw, {}), " Retry"] }) })] }));
32
32
  }
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-module.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AACvD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,GAAG,IAAI,GAIlB;IACC,OAAO,CACL,KAAC,iBAAiB,IAAC,SAAS,EAAE,SAAS,YACrC,KAAC,gBAAgB,IAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,GAAI,GAChD,CACrB,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,UAAU,GAIX;IACC,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,gCAAgC,CAAC,SAAS,CAAC,CAAA;IAE1D,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,iBAAa,iBAAiB,aAC1D,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,aAAa,IAAC,SAAS,EAAC,yBAAyB,GAAG,EACrD,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,yCAAgC,IACnD,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,YAAY,IAAI,yCAAyC,GAC3D,EACP,KAAC,GAAG,cACF,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,cACN,GACL,IACA,CACT,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,CACL,KAAC,KAAK,IAAC,SAAS,EAAC,+BAA+B,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,YAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,wCAErB,GACD,CACT,CAAA;IACH,CAAC;IAED,sFAAsF;IACtF,gFAAgF;IAChF,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,KAAC,oBAAoB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,GAAI,CAAA;IAC/F,CAAC;IAED,OAAO,CACL,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,qBAAqB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxB,CAC1B,CAAA;AACH,CAAC","sourcesContent":["import { AlertTriangle, RotateCcw } from 'lucide-react'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { RedirectsProvider, useRedirects } from './redirects-store'\nimport { useRedirectsCollectionViewSource } from './redirects-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Redirects module (EC-303). Editorial redirects as data — the reuse\n * proof (with the users rail) that the portable interaction surfaces serve any\n * rail: it renders the SAME `LoadedCollectionTable` for the list and the SAME\n * `DetailEditor` body (via {@link PortableDetailEditor}) for one redirect, over a\n * redirects {@link useRedirectsCollectionViewSource}. The redirects capability\n * profile (content-gated, create + remove, no publish/versions/locales) drives\n * which regions appear; the edit shell's explicit-save path (no publish) makes a\n * saved+enabled redirect live.\n *\n * `redirectId === null` is the list; a `redirectId` opens that redirect's detail\n * (routed via `studioPath.redirect`). No admin gate — redirects are content, so\n * viewers read and editors write (enforced at the chokepoint + Convex boundary).\n */\nexport function RedirectsModule({\n projectId,\n redirectId = null,\n}: {\n projectId: string\n redirectId?: string | null\n}) {\n return (\n <RedirectsProvider projectId={projectId}>\n <RedirectsSurface projectId={projectId} redirectId={redirectId} />\n </RedirectsProvider>\n )\n}\n\n/** The loaded surface: the portable list (or one redirect's detail) over the source. */\nfunction RedirectsSurface({\n projectId,\n redirectId,\n}: {\n projectId: string\n redirectId: string | null\n}) {\n const store = useRedirects()\n const source = useRedirectsCollectionViewSource(projectId)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"redirects-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">Redirects failed to load</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'The persistence backend is unreachable.'}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> Retry\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n Loading redirects…\n </Text>\n </Stack>\n )\n }\n\n // One redirect's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (redirectId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={redirectId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
1
+ {"version":3,"file":"redirects-module.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-module.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,cAAc,CAAA;AAEvD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAA;AAC/D,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACnE,OAAO,EAAE,gCAAgC,EAAE,MAAM,oCAAoC,CAAA;AACrF,OAAO,EAAE,sBAAsB,EAAE,MAAM,4CAA4C,CAAA;AACnF,OAAO,EAAE,qBAAqB,EAAE,MAAM,yBAAyB,CAAA;AAC/D,OAAO,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAA;AAE3D;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,eAAe,CAAC,EAC9B,SAAS,EACT,UAAU,GAAG,IAAI,EACjB,MAAM,GAMP;IACC,OAAO,CACL,KAAC,iBAAiB,IAAC,SAAS,EAAE,SAAS,YACrC,KAAC,gBAAgB,IAAC,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,EAAE,MAAM,GAAI,GAChE,CACrB,CAAA;AACH,CAAC;AAED,wFAAwF;AACxF,SAAS,gBAAgB,CAAC,EACxB,SAAS,EACT,UAAU,EACV,MAAM,GAKP;IACC,MAAM,KAAK,GAAG,YAAY,EAAE,CAAA;IAC5B,MAAM,MAAM,GAAG,gCAAgC,CAAC,SAAS,EAAE,MAAM,CAAC,CAAA;IAElE,IAAI,KAAK,CAAC,MAAM,KAAK,OAAO,EAAE,CAAC;QAC7B,OAAO,CACL,MAAC,KAAK,IAAC,GAAG,EAAC,GAAG,EAAC,SAAS,EAAC,KAAK,iBAAa,iBAAiB,aAC1D,MAAC,GAAG,IAAC,GAAG,EAAC,GAAG,aACV,KAAC,aAAa,IAAC,SAAS,EAAC,yBAAyB,GAAG,EACrD,KAAC,IAAI,IAAC,MAAM,EAAC,UAAU,yCAAgC,IACnD,EACN,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,YACzB,KAAK,CAAC,YAAY,IAAI,yCAAyC,GAC3D,EACP,KAAC,GAAG,cACF,MAAC,MAAM,IAAC,OAAO,EAAC,SAAS,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,EAAE,GAAG,EAAE,CAAC,KAAK,KAAK,CAAC,OAAO,EAAE,aACrE,KAAC,SAAS,KAAG,cACN,GACL,IACA,CACT,CAAA;IACH,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;QAC/B,OAAO,CACL,KAAC,KAAK,IAAC,SAAS,EAAC,+BAA+B,EAAC,KAAK,EAAC,QAAQ,EAAC,OAAO,EAAC,QAAQ,YAC9E,KAAC,IAAI,IAAC,IAAI,EAAC,IAAI,EAAC,IAAI,EAAC,OAAO,wCAErB,GACD,CACT,CAAA;IACH,CAAC;IAED,sFAAsF;IACtF,gFAAgF;IAChF,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,KAAC,oBAAoB,IAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,UAAU,EAAE,UAAU,GAAI,CAAA;IAC/F,CAAC;IAED,OAAO,CACL,KAAC,sBAAsB,IAAC,MAAM,EAAE,MAAM,YACpC,KAAC,qBAAqB,IAAC,SAAS,EAAE,SAAS,GAAI,GACxB,CAC1B,CAAA;AACH,CAAC","sourcesContent":["import { AlertTriangle, RotateCcw } from 'lucide-react'\nimport type { CollectionDef } from '@elytracms/core/cms-core'\nimport { Button, Row, Stack, Text } from '@elytracms/studio/ui'\nimport { RedirectsProvider, useRedirects } from './redirects-store'\nimport { useRedirectsCollectionViewSource } from './redirects-collection-view-source'\nimport { CollectionViewProvider } from '../collection-view/collection-view-context'\nimport { LoadedCollectionTable } from '../cms/collection-table'\nimport { PortableDetailEditor } from '../cms/detail-editor'\n\n/**\n * The studio Redirects module (EC-303). Editorial redirects as data — the reuse\n * proof (with the users rail) that the portable interaction surfaces serve any\n * rail: it renders the SAME `LoadedCollectionTable` for the list and the SAME\n * `DetailEditor` body (via {@link PortableDetailEditor}) for one redirect, over a\n * redirects {@link useRedirectsCollectionViewSource}. The redirects capability\n * profile (content-gated, create + remove, no publish/versions/locales) drives\n * which regions appear; the edit shell's explicit-save path (no publish) makes a\n * saved+enabled redirect live.\n *\n * `redirectId === null` is the list; a `redirectId` opens that redirect's detail\n * (routed via `studioPath.redirect`). No admin gate — redirects are content, so\n * viewers read and editors write (enforced at the chokepoint + Convex boundary).\n */\nexport function RedirectsModule({\n projectId,\n redirectId = null,\n schema,\n}: {\n projectId: string\n redirectId?: string | null\n /** EC-311: a mount-customized redirects def (built-in fields overridden by name). */\n schema?: CollectionDef\n}) {\n return (\n <RedirectsProvider projectId={projectId}>\n <RedirectsSurface projectId={projectId} redirectId={redirectId} schema={schema} />\n </RedirectsProvider>\n )\n}\n\n/** The loaded surface: the portable list (or one redirect's detail) over the source. */\nfunction RedirectsSurface({\n projectId,\n redirectId,\n schema,\n}: {\n projectId: string\n redirectId: string | null\n schema?: CollectionDef\n}) {\n const store = useRedirects()\n const source = useRedirectsCollectionViewSource(projectId, schema)\n\n if (store.status === 'error') {\n return (\n <Stack gap=\"2\" className=\"p-6\" data-testid=\"redirects-error\">\n <Row gap=\"2\">\n <AlertTriangle className=\"size-4 text-destructive\" />\n <Text weight=\"semibold\">Redirects failed to load</Text>\n </Row>\n <Text size=\"sm\" tone=\"muted\">\n {store.errorMessage ?? 'The persistence backend is unreachable.'}\n </Text>\n <Row>\n <Button variant=\"outline\" size=\"sm\" onClick={() => void store.refresh()}>\n <RotateCcw /> Retry\n </Button>\n </Row>\n </Stack>\n )\n }\n\n if (store.status === 'loading') {\n return (\n <Stack className=\"h-full place-items-center p-8\" align=\"center\" justify=\"center\">\n <Text size=\"sm\" tone=\"muted\">\n Loading redirects…\n </Text>\n </Stack>\n )\n }\n\n // One redirect's detail opens in the portable edit shell (AD-19); the list otherwise.\n // The shell mounts its own CollectionViewProvider, so only the list wraps here.\n if (redirectId !== null) {\n return <PortableDetailEditor source={source} projectId={projectId} documentId={redirectId} />\n }\n\n return (\n <CollectionViewProvider source={source}>\n <LoadedCollectionTable projectId={projectId} />\n </CollectionViewProvider>\n )\n}\n"]}
@@ -51,7 +51,10 @@ export function RedirectsProvider({ projectId, children, }) {
51
51
  from: '',
52
52
  to: '',
53
53
  permanent: true,
54
- enabled: false,
54
+ // New redirects default to enabled — the reason you create one is for it to
55
+ // work; `enabled` is really the switch for *pausing* an existing redirect. A
56
+ // half-filled draft can't go live anyway (save requires from/to).
57
+ enabled: true,
55
58
  createdAt: now,
56
59
  updatedAt: now,
57
60
  };
@@ -67,6 +70,7 @@ export function RedirectsProvider({ projectId, children, }) {
67
70
  to: input.to,
68
71
  ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),
69
72
  ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),
73
+ ...(input.meta !== undefined ? { meta: input.meta } : {}),
70
74
  });
71
75
  if (!result.ok)
72
76
  return { ok: false, message: result.error.message };
@@ -1 +1 @@
1
- {"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst RedirectsContext = createContext<RedirectsStore | null>(null)\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const status: RedirectsStore['status'] = query.isPending\n ? 'loading'\n : query.isError\n ? 'error'\n : 'ready'\n const errorMessage =\n query.isError && query.error instanceof Error ? query.error.message : undefined\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n enabled: false,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport function useRedirects(): RedirectsStore {\n const store = useContext(RedirectsContext)\n if (!store) throw new Error('useRedirects must be used within a <RedirectsProvider>.')\n return store\n}\n"]}
1
+ {"version":3,"file":"redirects-store.js","sourceRoot":"","sources":["../../../src/components/redirects/redirects-store.tsx"],"names":[],"mappings":"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","sourcesContent":["import { createContext, useCallback, useContext, useMemo, useState } from 'react'\nimport type { ReactNode } from 'react'\nimport type { RedirectEntry, SaveRedirectInput } from '@elytracms/core/persistence'\nimport { getStudioOperations } from '@elytracms/studio/lib/operations'\nimport type { OperationsClient } from '@elytracms/studio/lib/operations'\nimport { useOperationCache, useOperationQuery } from '@elytracms/studio/lib/query'\n\n/**\n * Editorial redirects store (EC-303). The redirects rail — data-backed 301/302\n * forwards — read + written through the shared operations client (`redirects.*`),\n * the same seam the users/assets stores use. Content-gated (a viewer reads, an\n * editor writes); no draft/published split — a saved+enabled redirect is live.\n *\n * The \"New\" flow keeps an unsaved redirect as a LOCAL draft (memory only) until\n * the first explicit save persists it — so an abandoned \"New\" never writes a stub\n * to the backend and never shows in the list. Drafts are reachable via {@link\n * RedirectsStore.get} (the detail editor loads one), never listed.\n */\n\nexport interface RedirectsStore {\n projectId: string\n status: 'loading' | 'ready' | 'error'\n errorMessage?: string\n /** The persisted redirects, ordered by source path. Unsaved drafts are excluded. */\n redirects: readonly RedirectEntry[]\n /** One redirect: a local unsaved draft if present, else a persisted record. */\n get(id: string): RedirectEntry | undefined\n /** Mint a blank in-memory draft and return its id (the \"New\" navigation target). */\n createDraft(): string\n /** Persist a create/modify; drops the matching draft on success. */\n save(\n input: SaveRedirectInput,\n ): Promise<{ ok: true; redirect: RedirectEntry } | { ok: false; message: string }>\n /** Delete a persisted redirect, or silently discard an unsaved draft. */\n remove(id: string): Promise<{ ok: true } | { ok: false; message: string }>\n /** Re-list from the backend. */\n refresh(): Promise<void>\n}\n\nconst RedirectsContext = createContext<RedirectsStore | null>(null)\n\nconst EMPTY_REDIRECTS: readonly RedirectEntry[] = []\n\nlet draftCounter = 0\n/** A collision-resistant client id for a new redirect (used as its real id on save). */\nfunction newDraftId(): string {\n draftCounter += 1\n return `redirect_${Date.now().toString(36)}_${draftCounter}`\n}\n\nfunction redirectsQueryKey(projectId: string): readonly [string, string] {\n return ['redirects', projectId]\n}\n\nasync function loadRedirects(\n operations: OperationsClient,\n projectId: string,\n): Promise<RedirectEntry[]> {\n const result = await operations.redirects.list({ projectId })\n if (!result.ok) throw new Error(result.error.message)\n return [...result.value]\n}\n\nfunction byFrom(a: RedirectEntry, b: RedirectEntry): number {\n return a.from < b.from ? -1 : a.from > b.from ? 1 : 0\n}\n\nexport function RedirectsProvider({\n projectId,\n children,\n}: {\n projectId: string\n children: ReactNode\n}) {\n const query = useOperationQuery<RedirectEntry[]>({\n queryKey: redirectsQueryKey(projectId),\n loader: (operations) => loadRedirects(operations, projectId),\n })\n const cache = useOperationCache()\n const refetch = query.refetch\n const persisted = query.data ?? EMPTY_REDIRECTS\n const status: RedirectsStore['status'] = query.isPending\n ? 'loading'\n : query.isError\n ? 'error'\n : 'ready'\n const errorMessage =\n query.isError && query.error instanceof Error ? query.error.message : undefined\n\n // In-memory unsaved \"New\" drafts, keyed by id. Not listed; only get()-able.\n const [drafts, setDrafts] = useState<Record<string, RedirectEntry>>({})\n\n const refresh = useCallback(async () => {\n await refetch()\n }, [refetch])\n\n const get = useCallback<RedirectsStore['get']>(\n (id) => drafts[id] ?? persisted.find((r) => r.id === id),\n [drafts, persisted],\n )\n\n const createDraft = useCallback<RedirectsStore['createDraft']>(() => {\n const id = newDraftId()\n const now = new Date().toISOString()\n const draft: RedirectEntry = {\n id,\n projectId,\n from: '',\n to: '',\n permanent: true,\n // New redirects default to enabled — the reason you create one is for it to\n // work; `enabled` is really the switch for *pausing* an existing redirect. A\n // half-filled draft can't go live anyway (save requires from/to).\n enabled: true,\n createdAt: now,\n updatedAt: now,\n }\n setDrafts((prev) => ({ ...prev, [id]: draft }))\n return id\n }, [projectId])\n\n const save = useCallback<RedirectsStore['save']>(\n async (input) => {\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.save({\n projectId,\n ...(input.id !== undefined ? { id: input.id } : {}),\n from: input.from,\n to: input.to,\n ...(input.permanent !== undefined ? { permanent: input.permanent } : {}),\n ...(input.enabled !== undefined ? { enabled: input.enabled } : {}),\n ...(input.meta !== undefined ? { meta: input.meta } : {}),\n })\n if (!result.ok) return { ok: false, message: result.error.message }\n const saved = result.value\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) => {\n const rest = (prev ?? []).filter((r) => r.id !== saved.id)\n return [...rest, saved].sort(byFrom)\n })\n // The record is now persisted — drop any matching draft.\n setDrafts((prev) => {\n if (!(saved.id in prev)) return prev\n const next = { ...prev }\n delete next[saved.id]\n return next\n })\n return { ok: true, redirect: saved }\n },\n [projectId, cache],\n )\n\n const remove = useCallback<RedirectsStore['remove']>(\n async (id) => {\n // An unsaved draft was never persisted — discard it locally, no backend call.\n if (drafts[id]) {\n setDrafts((prev) => {\n const next = { ...prev }\n delete next[id]\n return next\n })\n return { ok: true }\n }\n const { operations } = await getStudioOperations()\n const result = await operations.redirects.remove({ projectId, id })\n if (!result.ok) return { ok: false, message: result.error.message }\n cache.patch<RedirectEntry[]>(redirectsQueryKey(projectId), (prev) =>\n (prev ?? []).filter((r) => r.id !== id),\n )\n return { ok: true }\n },\n [projectId, cache, drafts],\n )\n\n const store = useMemo<RedirectsStore>(\n () => ({\n projectId,\n status,\n ...(errorMessage !== undefined ? { errorMessage } : {}),\n redirects: persisted,\n get,\n createDraft,\n save,\n remove,\n refresh,\n }),\n [projectId, status, errorMessage, persisted, get, createDraft, save, remove, refresh],\n )\n\n return <RedirectsContext.Provider value={store}>{children}</RedirectsContext.Provider>\n}\n\nexport function useRedirects(): RedirectsStore {\n const store = useContext(RedirectsContext)\n if (!store) throw new Error('useRedirects must be used within a <RedirectsProvider>.')\n return store\n}\n"]}
@@ -46,3 +46,10 @@ export type { AppViewContext, AppViewModule, SidebarNode } from './components/ap
46
46
  export { createStudio } from './lib/workspace';
47
47
  /** A ready-made example renderer; a consuming repo may use it or author its own. */
48
48
  export { colorFieldRenderer } from './components/cms/field-renderer';
49
+ /**
50
+ * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({
51
+ * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator
52
+ * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.
53
+ */
54
+ export { msg } from './lib/i18n';
55
+ export type { MessageCatalog, SourceMessage, StudioI18nConfig } from './lib/i18n';
@@ -43,4 +43,10 @@ export { nav } from './components/app-views/default-modules';
43
43
  export { createStudio } from './lib/workspace';
44
44
  /** A ready-made example renderer; a consuming repo may use it or author its own. */
45
45
  export { colorFieldRenderer } from './components/cms/field-renderer';
46
+ /**
47
+ * Studio-UI localization (EC-241): author a translation catalog for `defineStudio({
48
+ * i18n })`. `msg` is only needed to build an in-repo *source* catalog with translator
49
+ * hints; a plain `MessageCatalog` (flat key → string) is all a consumer supplies.
50
+ */
51
+ export { msg } from './lib/i18n';
46
52
  //# sourceMappingURL=customization.js.map
@@ -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","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"]}
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,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { documentDefaultLocale, resolveField, titleFieldOf } from '@elytracms/core/cms-core';
2
+ import { documentDefaultLocale, resolveField, resolveLabel, titleFieldOf } from '@elytracms/core/cms-core';
3
3
  import { FILTER_OPERATORS, compareListEntries, filterScalarOf, filterableFields, matchesCondition, validateWhere, } from '@elytracms/core/content';
4
4
  /**
5
5
  * The pure table-view model for the CMS collection table (EC-169).
@@ -67,7 +67,7 @@ export const STATE_OPTION = {
67
67
  export function filterableFieldOptions(collection) {
68
68
  const schemaOptions = filterableFields(collection).map((field) => ({
69
69
  field: field.name,
70
- label: field.form?.label ?? field.name,
70
+ label: resolveLabel(field.form?.label) ?? field.name,
71
71
  def: field,
72
72
  }));
73
73
  return [...schemaOptions, STATE_OPTION];