@elytracms/studio 0.0.98 → 0.0.99

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 (91) hide show
  1. package/convex/ingest.test.ts +84 -7
  2. package/convex/ingest.ts +110 -31
  3. package/convex/path-segment-refusal.test.ts +201 -0
  4. package/convex/records-column-transform.convex.test.ts +147 -0
  5. package/convex/schema.ts +1 -1
  6. package/dist/addon.d.ts +5 -3
  7. package/dist/addon.js +4 -3
  8. package/dist/addon.js.map +1 -1
  9. package/dist/components/app-views/default-modules.d.ts +10 -3
  10. package/dist/components/app-views/default-modules.js +5 -1
  11. package/dist/components/app-views/default-modules.js.map +1 -1
  12. package/dist/components/app-views/module.d.ts +15 -8
  13. package/dist/components/app-views/module.js.map +1 -1
  14. package/dist/components/builder/reuse-context.d.ts +29 -1
  15. package/dist/components/builder/reuse-context.js +58 -12
  16. package/dist/components/builder/reuse-context.js.map +1 -1
  17. package/dist/components/cms/cms-store.d.ts +3 -2
  18. package/dist/components/cms/cms-store.js +8 -2
  19. package/dist/components/cms/cms-store.js.map +1 -1
  20. package/dist/components/cms/field-control.js +15 -5
  21. package/dist/components/cms/field-control.js.map +1 -1
  22. package/dist/components/cms/use-document-draft.d.ts +4 -1
  23. package/dist/components/cms/use-document-draft.js +4 -1
  24. package/dist/components/cms/use-document-draft.js.map +1 -1
  25. package/dist/components/cms/use-publish-controller.d.ts +7 -0
  26. package/dist/components/cms/use-publish-controller.js +17 -3
  27. package/dist/components/cms/use-publish-controller.js.map +1 -1
  28. package/dist/components/cms/versions-pane.js +8 -5
  29. package/dist/components/cms/versions-pane.js.map +1 -1
  30. package/dist/components/delivery-health-banner.d.ts +34 -16
  31. package/dist/components/delivery-health-banner.js +59 -27
  32. package/dist/components/delivery-health-banner.js.map +1 -1
  33. package/dist/components/issue-severity.d.ts +3 -2
  34. package/dist/components/issue-severity.js.map +1 -1
  35. package/dist/components/nav-issues-registry.d.ts +39 -24
  36. package/dist/components/nav-issues-registry.js +50 -21
  37. package/dist/components/nav-issues-registry.js.map +1 -1
  38. package/dist/components/nav-issues.d.ts +23 -50
  39. package/dist/components/nav-issues.js +31 -59
  40. package/dist/components/nav-issues.js.map +1 -1
  41. package/dist/components/notifications/notice-center.d.ts +61 -0
  42. package/dist/components/notifications/notice-center.js +119 -0
  43. package/dist/components/notifications/notice-center.js.map +1 -0
  44. package/dist/components/notifications/notice-model.d.ts +137 -0
  45. package/dist/components/notifications/notice-model.js +107 -0
  46. package/dist/components/notifications/notice-model.js.map +1 -0
  47. package/dist/components/notifications/outcome-notice.d.ts +16 -0
  48. package/dist/components/notifications/outcome-notice.js +34 -0
  49. package/dist/components/notifications/outcome-notice.js.map +1 -0
  50. package/dist/components/notifications/shell-findings.d.ts +36 -0
  51. package/dist/components/notifications/shell-findings.js +140 -0
  52. package/dist/components/notifications/shell-findings.js.map +1 -0
  53. package/dist/components/rail-view-source.js +5 -0
  54. package/dist/components/rail-view-source.js.map +1 -1
  55. package/dist/components/records/record-rail.d.ts +10 -2
  56. package/dist/components/records/record-rail.js +74 -4
  57. package/dist/components/records/record-rail.js.map +1 -1
  58. package/dist/components/section-nav.d.ts +15 -3
  59. package/dist/components/section-nav.js +39 -147
  60. package/dist/components/section-nav.js.map +1 -1
  61. package/dist/components/studio-project-screen.js.map +1 -1
  62. package/dist/components/studio-shell.js +8 -3
  63. package/dist/components/studio-shell.js.map +1 -1
  64. package/dist/components/use-narrow-viewport.d.ts +11 -0
  65. package/dist/components/use-narrow-viewport.js +25 -0
  66. package/dist/components/use-narrow-viewport.js.map +1 -0
  67. package/dist/components/users/role-policy-drift-issues.d.ts +14 -7
  68. package/dist/components/users/role-policy-drift-issues.js +17 -8
  69. package/dist/components/users/role-policy-drift-issues.js.map +1 -1
  70. package/dist/lib/collection-view/collection-view-source.d.ts +4 -2
  71. package/dist/lib/collection-view/collection-view-source.js.map +1 -1
  72. package/dist/lib/engine-version.d.ts +1 -1
  73. package/dist/lib/engine-version.js +1 -1
  74. package/dist/lib/engine-version.js.map +1 -1
  75. package/dist/lib/i18n/messages.de.js +21 -3
  76. package/dist/lib/i18n/messages.de.js.map +1 -1
  77. package/dist/lib/i18n/messages.en.d.ts +19 -2
  78. package/dist/lib/i18n/messages.en.js +24 -4
  79. package/dist/lib/i18n/messages.en.js.map +1 -1
  80. package/dist/lib/notices/toast-store.d.ts +61 -0
  81. package/dist/lib/notices/toast-store.js +116 -0
  82. package/dist/lib/notices/toast-store.js.map +1 -0
  83. package/dist/lib/operations/cms-persistence.d.ts +24 -4
  84. package/dist/lib/operations/cms-persistence.js +417 -80
  85. package/dist/lib/operations/cms-persistence.js.map +1 -1
  86. package/dist/lib/persistence/convex-adapter.js +61 -30
  87. package/dist/lib/persistence/convex-adapter.js.map +1 -1
  88. package/dist/ui/toast.d.ts +24 -16
  89. package/dist/ui/toast.js +23 -32
  90. package/dist/ui/toast.js.map +1 -1
  91. package/package.json +3 -3
@@ -0,0 +1,25 @@
1
+ import { useSyncExternalStore } from 'react';
2
+ /**
3
+ * EC-690 — below this width the shell keeps the section rail icons-only (56 px) whatever the
4
+ * stored preference. Measured (wave-0100): at 390 px the expanded rail took 224 px, 57 % of
5
+ * the width, leaving the content 166 px — too little for the issues tray (bounded to the
6
+ * content area so it never covers the rail) or any editor.
7
+ */
8
+ export const NARROW_VIEWPORT_QUERY = '(max-width: 639px)';
9
+ function subscribe(onChange) {
10
+ if (typeof window === 'undefined' || !window.matchMedia)
11
+ return () => { };
12
+ const query = window.matchMedia(NARROW_VIEWPORT_QUERY);
13
+ query.addEventListener('change', onChange);
14
+ return () => query.removeEventListener('change', onChange);
15
+ }
16
+ const isNarrow = () => typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(NARROW_VIEWPORT_QUERY).matches;
17
+ /** Whether the viewport is narrow (`false` on the server: the rail renders as stored, then adapts). */
18
+ export function useNarrowViewport() {
19
+ return useSyncExternalStore(subscribe, isNarrow, () => false);
20
+ }
21
+ /** The ONE reader of "is the rail icons-only": the user's collapse, or a narrow viewport. */
22
+ export function effectiveRailCollapsed(collapsed, narrow) {
23
+ return collapsed || narrow;
24
+ }
25
+ //# sourceMappingURL=use-narrow-viewport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-narrow-viewport.js","sourceRoot":"","sources":["../../src/components/use-narrow-viewport.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,OAAO,CAAA;AAE5C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,oBAAoB,CAAA;AAEzD,SAAS,SAAS,CAAC,QAAoB;IACrC,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,MAAM,CAAC,UAAU;QAAE,OAAO,GAAG,EAAE,GAAE,CAAC,CAAA;IACxE,MAAM,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAA;IACtD,KAAK,CAAC,gBAAgB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;IAC1C,OAAO,GAAG,EAAE,CAAC,KAAK,CAAC,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;AAC5D,CAAC;AAED,MAAM,QAAQ,GAAG,GAAY,EAAE,CAC7B,OAAO,MAAM,KAAK,WAAW,IAAI,CAAC,CAAC,MAAM,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,CAAC,qBAAqB,CAAC,CAAC,OAAO,CAAA;AAE1G,uGAAuG;AACvG,MAAM,UAAU,iBAAiB;IAC/B,OAAO,oBAAoB,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAA;AAC/D,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,sBAAsB,CAAC,SAAkB,EAAE,MAAe;IACxE,OAAO,SAAS,IAAI,MAAM,CAAA;AAC5B,CAAC","sourcesContent":["import { useSyncExternalStore } from 'react'\n\n/**\n * EC-690 — below this width the shell keeps the section rail icons-only (56 px) whatever the\n * stored preference. Measured (wave-0100): at 390 px the expanded rail took 224 px, 57 % of\n * the width, leaving the content 166 px — too little for the issues tray (bounded to the\n * content area so it never covers the rail) or any editor.\n */\nexport const NARROW_VIEWPORT_QUERY = '(max-width: 639px)'\n\nfunction subscribe(onChange: () => void): () => void {\n if (typeof window === 'undefined' || !window.matchMedia) return () => {}\n const query = window.matchMedia(NARROW_VIEWPORT_QUERY)\n query.addEventListener('change', onChange)\n return () => query.removeEventListener('change', onChange)\n}\n\nconst isNarrow = (): boolean =>\n typeof window !== 'undefined' && !!window.matchMedia && window.matchMedia(NARROW_VIEWPORT_QUERY).matches\n\n/** Whether the viewport is narrow (`false` on the server: the rail renders as stored, then adapts). */\nexport function useNarrowViewport(): boolean {\n return useSyncExternalStore(subscribe, isNarrow, () => false)\n}\n\n/** The ONE reader of \"is the rail icons-only\": the user's collapse, or a narrow viewport. */\nexport function effectiveRailCollapsed(collapsed: boolean, narrow: boolean): boolean {\n return collapsed || narrow\n}\n"]}
@@ -16,16 +16,23 @@ export declare function studioRolePolicyDrift({ policy, declared, probe, }: {
16
16
  readonly declared: boolean;
17
17
  readonly probe: DeployedRolePolicyProbe;
18
18
  }): RolePolicyDriftVerdict | null;
19
- /** The rail-foot finding for a verdict (pure): the localized headline, then the verdict's own detail and fix. */
19
+ /**
20
+ * The issues-tray finding for a verdict (pure): the localized headline, then the verdict's own
21
+ * detail and fix. EC-690 (owner ruling (b)): a DEPLOYMENT finding, located at the setting the
22
+ * verdict reads — `ELYTRA_ROLE_POLICY` when the deployment enforces another (or an invalid)
23
+ * copy, which every such verdict fixes by re-provisioning it; the `roles:fingerprint` query
24
+ * when the deployment cannot answer at all — so the tray shows it to the roles that can act
25
+ * on a deployment (`project.administer`), not to every signed-in user.
26
+ */
20
27
  export declare function rolePolicyDriftNavIssues(verdict: RolePolicyDriftVerdict | null): readonly NavIssue[];
21
28
  /**
22
- * EC-605 — a visible issue, from boot, for every signed-in user when the connected
23
- * deployment enforces a different role policy than this project's `elytra.config.ts`
24
- * declares. While the two copies differ the deployment enforces ITS copy: a role only the
29
+ * EC-605 — a visible issue, from boot, for the project's admins (EC-690, owner ruling (b):
30
+ * a deployment finding) when the connected deployment enforces a different role policy than
31
+ * this project's `elytra.config.ts` declares. While the two copies differ the deployment enforces ITS copy: a role only the
25
32
  * config declares is refused everywhere, and a role whose grants the config reduced keeps
26
- * the deployed grants — so a drift is never left silent. Published to the rail's issues
27
- * foot (EC-616 `useNavIssues`, on every studio surface) for as long as the shell is
28
- * mounted; renders nothing itself.
33
+ * the deployed grants — so a drift is never left silent. Published to the shell's issues
34
+ * tray (EC-616 `useNavIssues`; the header badge, on every studio surface — EC-690) for as
35
+ * long as the shell is mounted; renders nothing itself.
29
36
  *
30
37
  * It reads the deployed fingerprint through the public `roles:fingerprint` query with the
31
38
  * same probe the doctor runs (`probeDeployedRolePolicy`), cached in the one query cache
@@ -1,6 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import { rolePolicyFingerprint } from '@elytracms/core/persistence';
3
- import { probeDeployedRolePolicy, rolePolicyDriftVerdict } from '@elytracms/core/export-sync/deployment-ready';
3
+ import { ROLE_POLICY_REPROVISION_FIX, probeDeployedRolePolicy, rolePolicyDriftVerdict, } from '@elytracms/core/export-sync/deployment-ready';
4
4
  import { translate } from '../../lib/i18n/index.js';
5
5
  import { projectRolePolicyFor } from '../../lib/operations/project-runtime-config.js';
6
6
  import { resolveBackendConfig } from '../../lib/persistence/backend-mode.js';
@@ -21,27 +21,36 @@ export function studioRolePolicyDrift({ policy, declared, probe, }) {
21
21
  const verdict = rolePolicyDriftVerdict({ configFingerprint: rolePolicyFingerprint(policy), configDeclared: declared, probe });
22
22
  return verdict.status === 'fail' ? verdict : null;
23
23
  }
24
- /** The rail-foot finding for a verdict (pure): the localized headline, then the verdict's own detail and fix. */
24
+ /**
25
+ * The issues-tray finding for a verdict (pure): the localized headline, then the verdict's own
26
+ * detail and fix. EC-690 (owner ruling (b)): a DEPLOYMENT finding, located at the setting the
27
+ * verdict reads — `ELYTRA_ROLE_POLICY` when the deployment enforces another (or an invalid)
28
+ * copy, which every such verdict fixes by re-provisioning it; the `roles:fingerprint` query
29
+ * when the deployment cannot answer at all — so the tray shows it to the roles that can act
30
+ * on a deployment (`project.administer`), not to every signed-in user.
31
+ */
25
32
  export function rolePolicyDriftNavIssues(verdict) {
26
33
  if (!verdict)
27
34
  return [];
35
+ const setting = verdict.fix === ROLE_POLICY_REPROVISION_FIX ? 'ELYTRA_ROLE_POLICY' : 'roles:fingerprint';
28
36
  return [
29
37
  {
30
38
  key: 'drift',
31
39
  severity: 'error',
32
40
  message: `${translate('shell.rolePolicyDrift.title')} ${verdict.detail}${verdict.fix ? ` ${verdict.fix}` : ''}`,
41
+ location: { kind: 'deployment', settings: [setting] },
33
42
  },
34
43
  ];
35
44
  }
36
45
  const NO_ISSUES = [];
37
46
  /**
38
- * EC-605 — a visible issue, from boot, for every signed-in user when the connected
39
- * deployment enforces a different role policy than this project's `elytra.config.ts`
40
- * declares. While the two copies differ the deployment enforces ITS copy: a role only the
47
+ * EC-605 — a visible issue, from boot, for the project's admins (EC-690, owner ruling (b):
48
+ * a deployment finding) when the connected deployment enforces a different role policy than
49
+ * this project's `elytra.config.ts` declares. While the two copies differ the deployment enforces ITS copy: a role only the
41
50
  * config declares is refused everywhere, and a role whose grants the config reduced keeps
42
- * the deployed grants — so a drift is never left silent. Published to the rail's issues
43
- * foot (EC-616 `useNavIssues`, on every studio surface) for as long as the shell is
44
- * mounted; renders nothing itself.
51
+ * the deployed grants — so a drift is never left silent. Published to the shell's issues
52
+ * tray (EC-616 `useNavIssues`; the header badge, on every studio surface — EC-690) for as
53
+ * long as the shell is mounted; renders nothing itself.
45
54
  *
46
55
  * It reads the deployed fingerprint through the public `roles:fingerprint` query with the
47
56
  * same probe the doctor runs (`probeDeployedRolePolicy`), cached in the one query cache
@@ -1 +1 @@
1
- {"version":3,"file":"role-policy-drift-issues.js","sourceRoot":"","sources":["../../../src/components/users/role-policy-drift-issues.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AAEnE,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,8CAA8C,CAAA;AAK9G,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAA;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAGrD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,MAAM,EACN,QAAQ,EACR,KAAK,GAKN;IACC,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;IAC7H,OAAO,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED,iHAAiH;AACjH,MAAM,UAAU,wBAAwB,CAAC,OAAsC;IAC7E,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IACvB,OAAO;QACL;YACE,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,GAAG,SAAS,CAAC,6BAA6B,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;SAChH;KACF,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAwB,EAAE,CAAA;AAEzC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAE,SAAS,EAAyB;IACxE,MAAM,OAAO,GAAG,oBAAoB,EAAE,CAAA;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAEhG,MAAM,KAAK,GAAG,iBAAiB,CAA0B;QACvD,QAAQ,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,CAAC;QACjD,MAAM,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,GAAI,CAAC;QAC3C,OAAO,EAAE,GAAG,KAAK,SAAS;KAC3B,CAAC,CAAA;IACF,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAA;IACxB,MAAM,MAAM,GAAG,OAAO,CACpB,GAAG,EAAE,CACH,KAAK;QACH,CAAC,CAAC,wBAAwB,CACtB,qBAAqB,CAAC;YACpB,MAAM,EAAE,oBAAoB,CAAC,SAAS,CAAC;YACvC,QAAQ,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,KAAK,KAAK,SAAS;YAC1D,KAAK;SACN,CAAC,CACH;QACH,CAAC,CAAC,SAAS,EACf,CAAC,KAAK,EAAE,SAAS,CAAC,CACnB,CAAA;IACD,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;IACpC,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { rolePolicyFingerprint } from '@elytracms/core/persistence'\nimport type { RolePolicy } from '@elytracms/core/persistence'\nimport { probeDeployedRolePolicy, rolePolicyDriftVerdict } from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeployedRolePolicyProbe,\n RolePolicyDriftVerdict,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport { translate } from '../../lib/i18n'\nimport { projectRolePolicyFor } from '../../lib/operations/project-runtime-config'\nimport { resolveBackendConfig } from '../../lib/persistence/backend-mode'\nimport { configProjectFor, deploymentUrlForSlug } from '../../lib/workspace'\nimport { useOperationQuery } from '../../lib/query'\nimport { useNavIssues } from '../nav-issues-registry'\nimport type { NavIssue } from '../nav-issues-registry'\n\n/**\n * EC-605 — the studio's half of the role-policy drift check: which verdict, if any, the\n * signed-in user is shown. Pure. The decision itself is core's ONE verdict\n * (`rolePolicyDriftVerdict`, the same `elytra doctor` prints as its `role-policy` row);\n * the studio surfaces only the `fail`s — a deployment enforcing another policy than the\n * config declares (or an invalid one, or none it can show while the config declares\n * roles). A `warn` (functions without `roles:fingerprint`, no roles declared) is a\n * developer's upgrade step the doctor names; an `unknown` is transient. `null` → nothing\n * to show.\n */\nexport function studioRolePolicyDrift({\n policy,\n declared,\n probe,\n}: {\n readonly policy: RolePolicy\n readonly declared: boolean\n readonly probe: DeployedRolePolicyProbe\n}): RolePolicyDriftVerdict | null {\n const verdict = rolePolicyDriftVerdict({ configFingerprint: rolePolicyFingerprint(policy), configDeclared: declared, probe })\n return verdict.status === 'fail' ? verdict : null\n}\n\n/** The rail-foot finding for a verdict (pure): the localized headline, then the verdict's own detail and fix. */\nexport function rolePolicyDriftNavIssues(verdict: RolePolicyDriftVerdict | null): readonly NavIssue[] {\n if (!verdict) return []\n return [\n {\n key: 'drift',\n severity: 'error',\n message: `${translate('shell.rolePolicyDrift.title')} ${verdict.detail}${verdict.fix ? ` ${verdict.fix}` : ''}`,\n },\n ]\n}\n\nconst NO_ISSUES: readonly NavIssue[] = []\n\n/**\n * EC-605 — a visible issue, from boot, for every signed-in user when the connected\n * deployment enforces a different role policy than this project's `elytra.config.ts`\n * declares. While the two copies differ the deployment enforces ITS copy: a role only the\n * config declares is refused everywhere, and a role whose grants the config reduced keeps\n * the deployed grants — so a drift is never left silent. Published to the rail's issues\n * foot (EC-616 `useNavIssues`, on every studio surface) for as long as the shell is\n * mounted; renders nothing itself.\n *\n * It reads the deployed fingerprint through the public `roles:fingerprint` query with the\n * same probe the doctor runs (`probeDeployedRolePolicy`), cached in the one query cache\n * like every studio read (AD-17). Playground has no deployed copy (the operations layer is\n * the only reader), so it never publishes there. A read that fails publishes nothing —\n * only a CONFIRMED drift does.\n */\nexport function RolePolicyDriftIssues({ projectId }: { projectId: string }) {\n const backend = resolveBackendConfig()\n const url = backend.kind === 'convex' ? deploymentUrlForSlug(projectId, backend.url) : undefined\n\n const query = useOperationQuery<DeployedRolePolicyProbe>({\n queryKey: ['roles', 'deployed-policy', projectId],\n loader: () => probeDeployedRolePolicy(url!),\n enabled: url !== undefined,\n })\n const probe = query.data\n const issues = useMemo(\n () =>\n probe\n ? rolePolicyDriftNavIssues(\n studioRolePolicyDrift({\n policy: projectRolePolicyFor(projectId),\n declared: configProjectFor(projectId)?.roles !== undefined,\n probe,\n }),\n )\n : NO_ISSUES,\n [probe, projectId],\n )\n useNavIssues('roles:policy', issues)\n return null\n}\n"]}
1
+ {"version":3,"file":"role-policy-drift-issues.js","sourceRoot":"","sources":["../../../src/components/users/role-policy-drift-issues.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAA;AAEnE,OAAO,EACL,2BAA2B,EAC3B,uBAAuB,EACvB,sBAAsB,GACvB,MAAM,8CAA8C,CAAA;AAKrD,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,6CAA6C,CAAA;AAClF,OAAO,EAAE,oBAAoB,EAAE,MAAM,oCAAoC,CAAA;AACzE,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAA;AAC5E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAA;AACnD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAGrD;;;;;;;;;GASG;AACH,MAAM,UAAU,qBAAqB,CAAC,EACpC,MAAM,EACN,QAAQ,EACR,KAAK,GAKN;IACC,MAAM,OAAO,GAAG,sBAAsB,CAAC,EAAE,iBAAiB,EAAE,qBAAqB,CAAC,MAAM,CAAC,EAAE,cAAc,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAA;IAC7H,OAAO,OAAO,CAAC,MAAM,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAA;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,wBAAwB,CAAC,OAAsC;IAC7E,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAA;IACvB,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,KAAK,2BAA2B,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,mBAAmB,CAAA;IACxG,OAAO;QACL;YACE,GAAG,EAAE,OAAO;YACZ,QAAQ,EAAE,OAAO;YACjB,OAAO,EAAE,GAAG,SAAS,CAAC,6BAA6B,CAAC,IAAI,OAAO,CAAC,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;YAC/G,QAAQ,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;SACtD;KACF,CAAA;AACH,CAAC;AAED,MAAM,SAAS,GAAwB,EAAE,CAAA;AAEzC;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,qBAAqB,CAAC,EAAE,SAAS,EAAyB;IACxE,MAAM,OAAO,GAAG,oBAAoB,EAAE,CAAA;IACtC,MAAM,GAAG,GAAG,OAAO,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,oBAAoB,CAAC,SAAS,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAEhG,MAAM,KAAK,GAAG,iBAAiB,CAA0B;QACvD,QAAQ,EAAE,CAAC,OAAO,EAAE,iBAAiB,EAAE,SAAS,CAAC;QACjD,MAAM,EAAE,GAAG,EAAE,CAAC,uBAAuB,CAAC,GAAI,CAAC;QAC3C,OAAO,EAAE,GAAG,KAAK,SAAS;KAC3B,CAAC,CAAA;IACF,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,CAAA;IACxB,MAAM,MAAM,GAAG,OAAO,CACpB,GAAG,EAAE,CACH,KAAK;QACH,CAAC,CAAC,wBAAwB,CACtB,qBAAqB,CAAC;YACpB,MAAM,EAAE,oBAAoB,CAAC,SAAS,CAAC;YACvC,QAAQ,EAAE,gBAAgB,CAAC,SAAS,CAAC,EAAE,KAAK,KAAK,SAAS;YAC1D,KAAK;SACN,CAAC,CACH;QACH,CAAC,CAAC,SAAS,EACf,CAAC,KAAK,EAAE,SAAS,CAAC,CACnB,CAAA;IACD,YAAY,CAAC,cAAc,EAAE,MAAM,CAAC,CAAA;IACpC,OAAO,IAAI,CAAA;AACb,CAAC","sourcesContent":["import { useMemo } from 'react'\nimport { rolePolicyFingerprint } from '@elytracms/core/persistence'\nimport type { RolePolicy } from '@elytracms/core/persistence'\nimport {\n ROLE_POLICY_REPROVISION_FIX,\n probeDeployedRolePolicy,\n rolePolicyDriftVerdict,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport type {\n DeployedRolePolicyProbe,\n RolePolicyDriftVerdict,\n} from '@elytracms/core/export-sync/deployment-ready'\nimport { translate } from '../../lib/i18n'\nimport { projectRolePolicyFor } from '../../lib/operations/project-runtime-config'\nimport { resolveBackendConfig } from '../../lib/persistence/backend-mode'\nimport { configProjectFor, deploymentUrlForSlug } from '../../lib/workspace'\nimport { useOperationQuery } from '../../lib/query'\nimport { useNavIssues } from '../nav-issues-registry'\nimport type { NavIssue } from '../nav-issues-registry'\n\n/**\n * EC-605 — the studio's half of the role-policy drift check: which verdict, if any, the\n * signed-in user is shown. Pure. The decision itself is core's ONE verdict\n * (`rolePolicyDriftVerdict`, the same `elytra doctor` prints as its `role-policy` row);\n * the studio surfaces only the `fail`s — a deployment enforcing another policy than the\n * config declares (or an invalid one, or none it can show while the config declares\n * roles). A `warn` (functions without `roles:fingerprint`, no roles declared) is a\n * developer's upgrade step the doctor names; an `unknown` is transient. `null` → nothing\n * to show.\n */\nexport function studioRolePolicyDrift({\n policy,\n declared,\n probe,\n}: {\n readonly policy: RolePolicy\n readonly declared: boolean\n readonly probe: DeployedRolePolicyProbe\n}): RolePolicyDriftVerdict | null {\n const verdict = rolePolicyDriftVerdict({ configFingerprint: rolePolicyFingerprint(policy), configDeclared: declared, probe })\n return verdict.status === 'fail' ? verdict : null\n}\n\n/**\n * The issues-tray finding for a verdict (pure): the localized headline, then the verdict's own\n * detail and fix. EC-690 (owner ruling (b)): a DEPLOYMENT finding, located at the setting the\n * verdict reads — `ELYTRA_ROLE_POLICY` when the deployment enforces another (or an invalid)\n * copy, which every such verdict fixes by re-provisioning it; the `roles:fingerprint` query\n * when the deployment cannot answer at all — so the tray shows it to the roles that can act\n * on a deployment (`project.administer`), not to every signed-in user.\n */\nexport function rolePolicyDriftNavIssues(verdict: RolePolicyDriftVerdict | null): readonly NavIssue[] {\n if (!verdict) return []\n const setting = verdict.fix === ROLE_POLICY_REPROVISION_FIX ? 'ELYTRA_ROLE_POLICY' : 'roles:fingerprint'\n return [\n {\n key: 'drift',\n severity: 'error',\n message: `${translate('shell.rolePolicyDrift.title')} ${verdict.detail}${verdict.fix ? ` ${verdict.fix}` : ''}`,\n location: { kind: 'deployment', settings: [setting] },\n },\n ]\n}\n\nconst NO_ISSUES: readonly NavIssue[] = []\n\n/**\n * EC-605 — a visible issue, from boot, for the project's admins (EC-690, owner ruling (b):\n * a deployment finding) when the connected deployment enforces a different role policy than\n * this project's `elytra.config.ts` declares. While the two copies differ the deployment enforces ITS copy: a role only the\n * config declares is refused everywhere, and a role whose grants the config reduced keeps\n * the deployed grants — so a drift is never left silent. Published to the shell's issues\n * tray (EC-616 `useNavIssues`; the header badge, on every studio surface — EC-690) for as\n * long as the shell is mounted; renders nothing itself.\n *\n * It reads the deployed fingerprint through the public `roles:fingerprint` query with the\n * same probe the doctor runs (`probeDeployedRolePolicy`), cached in the one query cache\n * like every studio read (AD-17). Playground has no deployed copy (the operations layer is\n * the only reader), so it never publishes there. A read that fails publishes nothing —\n * only a CONFIRMED drift does.\n */\nexport function RolePolicyDriftIssues({ projectId }: { projectId: string }) {\n const backend = resolveBackendConfig()\n const url = backend.kind === 'convex' ? deploymentUrlForSlug(projectId, backend.url) : undefined\n\n const query = useOperationQuery<DeployedRolePolicyProbe>({\n queryKey: ['roles', 'deployed-policy', projectId],\n loader: () => probeDeployedRolePolicy(url!),\n enabled: url !== undefined,\n })\n const probe = query.data\n const issues = useMemo(\n () =>\n probe\n ? rolePolicyDriftNavIssues(\n studioRolePolicyDrift({\n policy: projectRolePolicyFor(projectId),\n declared: configProjectFor(projectId)?.roles !== undefined,\n probe,\n }),\n )\n : NO_ISSUES,\n [probe, projectId],\n )\n useNavIssues('roles:policy', issues)\n return null\n}\n"]}
@@ -231,9 +231,11 @@ export interface DocumentWrites {
231
231
  /**
232
232
  * Write the whole document at `revision` — the revision of the view the snapshot was
233
233
  * authored on (EC-647; a document-level key the value-position grammar has no position
234
- * for — the slot `alias`). A move since is refused, never overwritten.
234
+ * for — the slot `alias`). A move since is refused, never overwritten. `authoredOn`: the
235
+ * document the snapshot was made from — a refusal is re-sent per position against it
236
+ * (EC-686).
235
237
  */
236
- replace(doc: CmsDocument, revision: number | undefined): void;
238
+ replace(doc: CmsDocument, revision: number | undefined, authoredOn?: CmsDocument): void;
237
239
  /**
238
240
  * EC-595 point 7 — follow ONE document while it is open: every pushed row is adopted
239
241
  * under the local edits (the editor rebases), and the presence on it is delivered to
@@ -1 +1 @@
1
- {"version":3,"file":"collection-view-source.js","sourceRoot":"","sources":["../../../src/lib/collection-view/collection-view-source.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAoTvD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA4B,EAC5B,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACjG,CAAC;AAyDD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA4B;IACjE,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE;QAClC,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO;KACtC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n ValuePath,\n DocumentUrl,\n FieldDef,\n Locale,\n LocaleConfig,\n LocalizedString,\n RestoreScope,\n} from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { CmsDocumentRecord, PresenceEntry, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { CommitOutcome, DocumentConflict } from '../operations/cms-persistence'\nimport type { TableViewState } from '../cms-views/view-model'\nimport { pseudoColumns } from '../cms-views/view-model'\nimport type { ReadState } from '../query'\n// The concrete inline-edit + bulk vocabulary (Stream A relocates it into `lib` as it settles).\nimport type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit'\nimport type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions'\nimport type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store'\n\n/**\n * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,\n * vision AD-19).\n *\n * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are\n * the studio's universal interaction surfaces. Historically they read the\n * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.\n * This interface is the data-agnostic seam they render over instead, so the SAME\n * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app\n * edit-shell, and any future rail — no hand-rolled UI per rail.\n *\n * **Shape:** the source is scoped to ONE collection (it carries `schema` +\n * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces\n * actually use — dropping the `collectionId` argument (the source already knows\n * it). Keeping the names/signatures aligned makes the CMS adapter a\n * behaviour-preserving pass-through and the migration near-mechanical.\n *\n * **Referential stability:** the surfaces put `documents`, `isPublished`,\n * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST\n * return stable references across renders (delegate to the underlying store's\n * already-stable methods; never wrap in a fresh closure each render) or memos\n * bust and the inline-edit order snapshot / perf regress.\n *\n * **Capabilities:** flags gate whole shell regions AND method presence — a source\n * with `capabilities.versions === false` (the users rail) never mounts the\n * Versions pane, so no `listVersions` read fires on credential records. Optional\n * method groups are present iff the flag is set.\n */\nexport interface CollectionViewSource {\n /** The rail/collection this source serves. */\n readonly collectionId: string\n /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */\n readonly schema: CollectionDef\n /** Which regions/methods this source supports. */\n readonly capabilities: CollectionViewCapabilities\n\n /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */\n readonly projectId?: string\n /**\n * The collection id assets live in (for a cell/field's `titleOf` label fallback\n * when {@link resolveAsset} finds no record). Present only for sources that\n * reference assets — a rail without assets (users) omits it.\n */\n readonly assetCollectionId?: string\n /** The ambient locale lens (only meaningful when `capabilities.locales`). */\n readonly localeConfig: LocaleConfig\n readonly activeLocale: Locale\n\n // --- Reads -----------------------------------------------------------------\n /** The full row set (view mechanics filter/sort this client-side). */\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n /** A relation/asset target's display title (hides the string-vs-function `titleField`). */\n titleOf(ref: DocumentRef): string\n /**\n * List the candidate records of a relation field's declared `target` collection\n * — the pick-list for a relation picker (EC-072). References are first-class and\n * collection-based: the field's schema declares which collection(s) it targets,\n * and this resolves those rows for ANY target (cross-collection), not just this\n * source's own collection. The caller passes `field.target`; a multi-target field\n * (future) merges one call per declared target. An unknown/empty target yields `[]`.\n */\n referenceCandidates(collectionId: string): CmsDocument[]\n /**\n * The workspace schema a reference/asset can point at — the schema counterpart to\n * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's\n * fields (an asset field editing the asset collection's own metadata; the reuse\n * capability correlating embed manifests against their target collection). A\n * reference is collection-based, so resolving one sometimes needs the target's\n * schema, not just its rows.\n */\n readonly collections: readonly CollectionDef[]\n isFieldReadOnly(field: FieldDef): boolean\n /**\n * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`\n * (visible but read-only), or `hidden` (not rendered for this principal). This is\n * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`\n * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a\n * sensitive field (`hidden`). A single tri-state (never two independent flags) so\n * the nonsensical writable-but-invisible state can't be expressed. Optional:\n * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open\n * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the\n * source; the starter's policy flows into its construction (engine = mechanism).\n * Prefer {@link resolveFieldAccess} over calling this directly (it applies the\n * default-open fallback).\n */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Resolve an asset id to its display metadata for a cell/field, or `undefined`\n * when there is no asset backend (a source without assets degrades to a label\n * via {@link titleOf}, matching today's \"no AssetsProvider\" behaviour).\n */\n resolveAsset?(id: string): AssetDisplay | undefined\n /**\n * How this rail's records are addressed in the EC-155 reference index — the\n * target shape the delete/unpublish confirmation's inbound pre-flight queries\n * (EC-433). Absent ⇒ the document shape (`{ type: 'document', id: 'collection:id' }`),\n * which every `cmsDocuments` collection uses. The assets rail supplies\n * `{ type: 'asset' }` so its list/bulk delete confirmation enumerates the SAME\n * inbound references its detail surface does — a document-shaped lookup would\n * report a referenced asset as \"not referenced\" (the EC-319 objection that\n * originally kept `remove` off the assets rail).\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */\n readonly lastError: OperationError | null\n clearLastError(): void\n /**\n * Land any queued/optimistic writes so a subsequent pre-flight READ sees them\n * (the reference index is maintained on write). A source with no async\n * persistence omits it — its reads are already authoritative.\n */\n flush?(): Promise<void>\n\n /**\n * The studio route splat for a record's detail surface (the row link + the\n * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.\n * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail\n * has no detail surface, so rows render as plain text (no navigation).\n */\n detailPath?(id: string): string\n /**\n * The studio route splat for this rail's LIST surface — the edit shell's\n * \"back to list\" breadcrumb crumb and its post-delete navigation target, e.g.\n * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never\n * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`\n * ⇒ the crumb is unlinked and delete falls back to the project index.\n */\n readonly listPath?: string\n\n // --- View state (list) -----------------------------------------------------\n viewState(): TableViewState\n setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void\n\n // --- Writes ----------------------------------------------------------------\n create(): DocumentRef\n /**\n * EC-595 A2 — HOW this source's records are written: exactly one model per source,\n * so a surface can never write a revisioned document the record way or vice versa.\n * `record` — the explicit-save rails (users, redirects, assets, the account): the\n * editor's Save button hands the whole record over. `document` — the CMS: every\n * writer emits patches at its source, committed on quiescence and merged per path\n * at the store; the editor shows per-field conflicts and follows the subscription.\n */\n readonly writes: CollectionWrites\n remove(ref: DocumentRef): void\n /**\n * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one\n * command PER item, per-item outcomes enumerated in the report (partial failure\n * is never silent).\n */\n bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>\n\n // --- Capability method groups (present iff the matching flag is set) --------\n /** `publish` — published-pool predicate + publish/unpublish. */\n readonly publishing?: PublishingCapability\n /**\n * Per-document timestamps (EC-445) — the data behind the `$updatedAt`\n * pseudo-column (and, once its data path lands, `$publishedAt`). Present iff the\n * source materializes them (the CMS source, sourced through session-hydration);\n * a rail that carries no timestamps omits it, so those pseudo-columns simply don't\n * apply (see {@link pseudoColumnsForSource}). The accessor shape mirrors\n * {@link PublishingCapability.isPublished} — a per-ref lookup, referentially stable.\n */\n readonly timestamps?: DocumentTimestampsCapability\n /** `versions` — version history + restore. */\n readonly versioning?: VersioningCapability\n /** `inlineEdit` — the list's single-command optimistic cell write. */\n readonly inlineEditing?: InlineEditCapability\n /**\n * Config-as-code named lenses (EC-246b) the header view selector switches\n * between. Optional: a source that declares no named views (a non-CMS rail\n * like users) omits it, and the selector renders nothing (single default).\n */\n readonly views?: NamedViewsCapability\n /**\n * `importExport` — JSON round-trip of the collection's records. CMS-specific;\n * a rail whose records are credential-bearing (users) omits it, so neither the\n * import dialog nor the export action mounts (identities aren't dumped to JSON).\n */\n readonly imports?: ImportCapability\n}\n\n/** EC-595 A2 — the write model of a source (see {@link CollectionViewSource.writes}). */\nexport type CollectionWrites = RecordWrites | DocumentWrites\n\n/**\n * An explicit-save rail: the editor's Save button writes the record. `base` (EC-616) is the\n * record as the editor OPENED it (or as of their last successful save) — a rail that can\n * write per column sends only what differs from it, so a column another writer changed\n * meanwhile is kept; absent for a record being created.\n */\nexport interface RecordWrites {\n readonly kind: 'record'\n save(doc: CmsDocument, base?: CmsDocument): void\n}\n\n/**\n * The revisioned-document write model (EC-595, ratified 2026-10-24): patches at the\n * source, the persister's queue, per-field conflicts, the document subscription.\n */\nexport interface DocumentWrites {\n readonly kind: 'document'\n /**\n * Commit the patches a writer made; resolves once acknowledged (`undefined` in a\n * fixture session). `bases` — REQUIRED, per patch: the revision of the view the writer\n * authored it against (EC-647 — never the session's current one by default; a write\n * that names none is refused).\n */\n commit(\n ref: DocumentRef,\n patches: readonly DocumentPatch[],\n bases: readonly (number | undefined)[],\n ): Promise<CommitOutcome | undefined>\n /** The revision the session's view of a document is at now. */\n revisionOf(ref: DocumentRef): number | undefined\n /**\n * EC-647 — the revision a document value the session SHOWED is at (`undefined` for a\n * value it never showed, or a provisional one): a writer holding the value it displayed\n * holds its revision. EC-667 — a view showing a pending write is a composite: with\n * `path`, the revision the content at that position was authored against (an editor's\n * first keystroke there); without it, the EARLIEST of any position the view shows — the\n * token of a whole-document writer.\n */\n revisionOfView(view: CmsDocument, path?: ValuePath): number | undefined\n /** Create a document from a whole value (insert-if-absent): duplicate. */\n insert(doc: CmsDocument): void\n /** A fresh document id from the session's injected factory (EC-578). */\n newId(): string\n /** The open per-field conflicts on a document. */\n conflicts(ref: DocumentRef): readonly DocumentConflict[]\n resolveConflict(ref: DocumentRef, conflictId: string, choice: 'theirs' | 'mine'): void\n /** Adopt a row the document subscription pushed (local edits win on their paths until flushed). */\n adoptSnapshot(ref: DocumentRef, record: CmsDocumentRecord | undefined): void\n /** The forced flush's persistence half: await every queued write of the document. */\n flush(ref: DocumentRef): Promise<void>\n /**\n * Write the whole document at `revision` — the revision of the view the snapshot was\n * authored on (EC-647; a document-level key the value-position grammar has no position\n * for — the slot `alias`). A move since is refused, never overwritten.\n */\n replace(doc: CmsDocument, revision: number | undefined): void\n /**\n * EC-595 point 7 — follow ONE document while it is open: every pushed row is adopted\n * under the local edits (the editor rebases), and the presence on it is delivered to\n * `onPresence`. Returns the unsubscribe function.\n */\n subscribe(ref: DocumentRef, onPresence: (presence: readonly PresenceEntry[]) => void): () => void\n /** Thin presence (Stage A): this session is on the document, in `path`'s field (or on no field). */\n reportPresence(ref: DocumentRef, path: ValuePath | undefined): void\n /** This session left the document. */\n clearPresence(ref: DocumentRef): void\n /** This session's presence id (one per tab), so the editor can leave itself out. */\n readonly presenceSessionId: string\n}\n\n/**\n * The capability matrix. Each flag gates a shell region AND the presence of its\n * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`\n * (credential caution), `create:false` (identity minted by sign-up), and no\n * `liveUrl`/`hierarchy`.\n */\nexport interface CollectionViewCapabilities {\n readonly publish: boolean\n readonly versions: boolean\n readonly locales: boolean\n readonly inlineEdit: boolean\n readonly create: boolean\n /**\n * Duplicate-document action (EC-442, amended same day). Deliberately its own\n * flag, never derived from `create`: the owner call (2026-07-17) is CMS\n * collections ONLY — no primitive rail offers Duplicate for now (identities\n * and uploads aren't copyable content; redirects deferred until requested as\n * a product feature).\n */\n readonly duplicate: boolean\n readonly remove: boolean\n readonly bulk: boolean\n /** The derived live-URL column (published pool × deployment routes). */\n readonly liveUrl: boolean\n /** Parent-self-relation tree mode (hierarchical collections only). */\n readonly hierarchy: boolean\n /** JSON import/export of the collection's records (off for credential rails). */\n readonly importExport: boolean\n /**\n * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The\n * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.\n */\n readonly adminOnly?: boolean\n}\n\n/**\n * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility\n * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not\n * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;\n * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is\n * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).\n */\nexport type FieldAccess = 'edit' | 'read' | 'hidden'\n\n/**\n * Resolve a field's effective principal access for a source — its {@link\n * CollectionViewSource.fieldAccess} policy when present, else derived from the\n * boolean `isFieldReadOnly` (default-open). The one place the shell asks \"may this\n * principal edit / see this field?\", so the default-open fallback lives in one spot.\n */\nexport function resolveFieldAccess(\n source: CollectionViewSource,\n field: FieldDef,\n record?: CmsDocument,\n): FieldAccess {\n return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit')\n}\n\n/**\n * Asset display metadata a cell/field renders (thumbnail or labelled badge). EC-587:\n * named apart from core's `ResolvedAsset` (the delivery shape) — this is the studio's\n * label/thumbnail slice; its `alt` is the one reader's (`assetAlt`).\n */\nexport interface AssetDisplay {\n readonly url?: string\n readonly contentType?: string\n readonly title?: string\n readonly filename?: string\n readonly alt?: string\n}\n\n/** `publish` capability. */\nexport interface PublishingCapability {\n /** Published-ness from the version pointer (NOT a row field). */\n isPublished(ref: DocumentRef): boolean\n /**\n * Does this PUBLISHED document's working draft carry unpublished changes\n * (EC-449)? The cheap per-row signal behind the amber \"changed\" `$state` dot —\n * sourced from the MATERIALIZED `hasUnpublishedChanges` record flag (computed\n * once at the write chokepoint), never a per-row snapshot compare. Optional: a\n * source without the signal omits it, and the list derives only\n * published/draft (exactly the pre-EC-449 rendering). Same referentially-stable\n * per-ref accessor shape as {@link isPublished}.\n */\n hasUnpublishedChanges?(ref: DocumentRef): boolean\n publish(ref: DocumentRef): Promise<void>\n unpublish(ref: DocumentRef): Promise<void>\n /**\n * Live URLs for the routable documents, keyed by id — derived from the\n * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't\n * leak into a live link (EC-205/EC-216). `null` when the collection is not\n * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.\n *\n * EC-628: a {@link ReadState} — the URL map only when the published read answered; a\n * failed read is `error` with its sentence, which the URL column shows in every row\n * (never the \"not published\" dash an empty map would give).\n */\n liveUrls?(): ReadState<ReadonlyMap<string, DocumentUrl>> | null\n}\n\n/**\n * `timestamps` capability (EC-445/EC-447): per-document record timestamps as ISO\n * strings, or `undefined` when a document carries none. The `$updatedAt`/`$publishedAt`\n * pseudo-columns read + sort by these. `updatedAt` is the last-write instant;\n * `publishedAt` (EC-447) is the FIRST-publish instant — set-once on publish, cleared on\n * unpublish, so it is `undefined` for a draft / never-published / currently-unpublished\n * document (the column shows EmDash).\n */\nexport interface DocumentTimestampsCapability {\n updatedAt(ref: DocumentRef): string | undefined\n publishedAt(ref: DocumentRef): string | undefined\n}\n\n/**\n * The reserved pseudo-columns applicable to a source's list (EC-445), derived from\n * its capabilities: `$updatedAt` iff it materializes timestamps, `$state` iff it has a\n * publish lifecycle, `$url` iff it resolves delivery URLs. The one place the list\n * surface + the Display picker agree on which pseudo-columns exist for a source, so\n * the column universe never drifts between where columns render and where they toggle.\n */\nexport function pseudoColumnsForSource(source: CollectionViewSource): string[] {\n return pseudoColumns(source.schema, {\n timestamps: source.timestamps !== undefined,\n publishing: source.publishing !== undefined,\n routable: source.capabilities.liveUrl,\n })\n}\n\n/** `versions` capability. */\nexport interface VersioningCapability {\n restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>\n /**\n * The document's recorded version history + live pin. Folds the flush-before-read\n * ordering INTO the loader (the adapter's concern), so every fetch lists an\n * authoritative history regardless of which surface triggered the change (EC-224).\n */\n load(ref: DocumentRef): Promise<DocumentVersionsView>\n}\n\n/** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */\nexport interface InlineEditCapability {\n commitField(input: CellCommitInput): Promise<CellCommitOutcome>\n}\n\n/** A named lens the header selector offers — id + display name only (the header never needs its state). */\nexport interface NamedView {\n readonly id: string\n /** Display name; a {@link LocalizedString} (EC-343), resolved at the render site like `form.label`. */\n readonly name: LocalizedString\n}\n\n/** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */\nexport interface ImportCapability {\n importDocuments(items: unknown[]): ImportReport\n}\n\n/**\n * `views` capability: config-as-code named lenses (EC-246b). The header selector\n * only *picks* a lens — it never creates/edits/deletes one (shared views are code).\n */\nexport interface NamedViewsCapability {\n /** The built-in default + config-declared named views. */\n list(): readonly NamedView[]\n /** The currently-selected view id. */\n activeId(): string\n /** Switch views (resets the working table state to the view's baseline). */\n setActive(id: string): void\n}\n\nexport type { CellCommitInput, CellCommitOutcome, ImportReport }\n"]}
1
+ {"version":3,"file":"collection-view-source.js","sourceRoot":"","sources":["../../../src/lib/collection-view/collection-view-source.ts"],"names":[],"mappings":"AAkBA,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAsTvD;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAChC,MAA4B,EAC5B,KAAe,EACf,MAAoB;IAEpB,OAAO,MAAM,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAA;AACjG,CAAC;AAyDD;;;;;;GAMG;AACH,MAAM,UAAU,sBAAsB,CAAC,MAA4B;IACjE,OAAO,aAAa,CAAC,MAAM,CAAC,MAAM,EAAE;QAClC,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,UAAU,EAAE,MAAM,CAAC,UAAU,KAAK,SAAS;QAC3C,QAAQ,EAAE,MAAM,CAAC,YAAY,CAAC,OAAO;KACtC,CAAC,CAAA;AACJ,CAAC","sourcesContent":["import type {\n CmsDocument,\n CollectionDef,\n DocumentPatch,\n DocumentRef,\n ValuePath,\n DocumentUrl,\n FieldDef,\n Locale,\n LocaleConfig,\n LocalizedString,\n RestoreScope,\n} from '@elytracms/core/cms-core'\nimport type { CmsValidationIssue } from '@elytracms/core/cms-core'\nimport type { OperationError } from '@elytracms/core/operations'\nimport type { CmsDocumentRecord, PresenceEntry, ReferenceTarget } from '@elytracms/core/persistence'\nimport type { CommitOutcome, DocumentConflict } from '../operations/cms-persistence'\nimport type { TableViewState } from '../cms-views/view-model'\nimport { pseudoColumns } from '../cms-views/view-model'\nimport type { ReadState } from '../query'\n// The concrete inline-edit + bulk vocabulary (Stream A relocates it into `lib` as it settles).\nimport type { CellCommitInput, CellCommitOutcome } from '../../components/cms/cell-edit'\nimport type { BulkAction, BulkReport, BulkSetField } from '../../components/cms/bulk-actions'\nimport type { DocumentVersionsView, ImportReport } from '../../components/cms/cms-store'\n\n/**\n * `CollectionViewSource` — the studio's UNIVERSAL interaction seam (EC-240,\n * vision AD-19).\n *\n * The list surface (`CollectionTable`) and the edit-shell (`DetailEditor`) are\n * the studio's universal interaction surfaces. Historically they read the\n * concrete `CmsStore` through `useCms()`, so they only served `cmsDocuments`.\n * This interface is the data-agnostic seam they render over instead, so the SAME\n * surfaces drive the users rail (EC-240), redirects (EC-303), the Components app\n * edit-shell, and any future rail — no hand-rolled UI per rail.\n *\n * **Shape:** the source is scoped to ONE collection (it carries `schema` +\n * `collectionId`), and its methods mirror the subset of `CmsStore` the surfaces\n * actually use — dropping the `collectionId` argument (the source already knows\n * it). Keeping the names/signatures aligned makes the CMS adapter a\n * behaviour-preserving pass-through and the migration near-mechanical.\n *\n * **Referential stability:** the surfaces put `documents`, `isPublished`,\n * `localeConfig`, etc. in `useMemo` dependency arrays. An implementation MUST\n * return stable references across renders (delegate to the underlying store's\n * already-stable methods; never wrap in a fresh closure each render) or memos\n * bust and the inline-edit order snapshot / perf regress.\n *\n * **Capabilities:** flags gate whole shell regions AND method presence — a source\n * with `capabilities.versions === false` (the users rail) never mounts the\n * Versions pane, so no `listVersions` read fires on credential records. Optional\n * method groups are present iff the flag is set.\n */\nexport interface CollectionViewSource {\n /** The rail/collection this source serves. */\n readonly collectionId: string\n /** The schema lens: fields, form tabs/groups, `titleField`, display nouns. */\n readonly schema: CollectionDef\n /** Which regions/methods this source supports. */\n readonly capabilities: CollectionViewCapabilities\n\n /** The project scope (asset picker, published-pool queries). Absent in fixture sessions. */\n readonly projectId?: string\n /**\n * The collection id assets live in (for a cell/field's `titleOf` label fallback\n * when {@link resolveAsset} finds no record). Present only for sources that\n * reference assets — a rail without assets (users) omits it.\n */\n readonly assetCollectionId?: string\n /** The ambient locale lens (only meaningful when `capabilities.locales`). */\n readonly localeConfig: LocaleConfig\n readonly activeLocale: Locale\n\n // --- Reads -----------------------------------------------------------------\n /** The full row set (view mechanics filter/sort this client-side). */\n documents(): CmsDocument[]\n get(ref: DocumentRef): CmsDocument | undefined\n /** A relation/asset target's display title (hides the string-vs-function `titleField`). */\n titleOf(ref: DocumentRef): string\n /**\n * List the candidate records of a relation field's declared `target` collection\n * — the pick-list for a relation picker (EC-072). References are first-class and\n * collection-based: the field's schema declares which collection(s) it targets,\n * and this resolves those rows for ANY target (cross-collection), not just this\n * source's own collection. The caller passes `field.target`; a multi-target field\n * (future) merges one call per declared target. An unknown/empty target yields `[]`.\n */\n referenceCandidates(collectionId: string): CmsDocument[]\n /**\n * The workspace schema a reference/asset can point at — the schema counterpart to\n * {@link referenceCandidates}. Needed for the cases that read a TARGET collection's\n * fields (an asset field editing the asset collection's own metadata; the reuse\n * capability correlating embed manifests against their target collection). A\n * reference is collection-based, so resolving one sometimes needs the target's\n * schema, not just its rows.\n */\n readonly collections: readonly CollectionDef[]\n isFieldReadOnly(field: FieldDef): boolean\n /**\n * Principal-based field access (EC-240, vision AD-18): `edit` (writable), `read`\n * (visible but read-only), or `hidden` (not rendered for this principal). This is\n * IDENTITY-driven and distinct from a field's content-conditional `hidden(values)`\n * — e.g. an editor's own `role` resolves to `read`, a viewer never SEES a\n * sensitive field (`hidden`). A single tri-state (never two independent flags) so\n * the nonsensical writable-but-invisible state can't be expressed. Optional:\n * absent ⇒ derived from {@link isFieldReadOnly} (`read`|`edit`), i.e. default-open\n * (permissions RESTRICT, never grant a baseline). Per-rail policy lives on the\n * source; the starter's policy flows into its construction (engine = mechanism).\n * Prefer {@link resolveFieldAccess} over calling this directly (it applies the\n * default-open fallback).\n */\n fieldAccess?(field: FieldDef, record?: CmsDocument): FieldAccess\n validate(doc: CmsDocument): CmsValidationIssue[]\n /**\n * Resolve an asset id to its display metadata for a cell/field, or `undefined`\n * when there is no asset backend (a source without assets degrades to a label\n * via {@link titleOf}, matching today's \"no AssetsProvider\" behaviour).\n */\n resolveAsset?(id: string): AssetDisplay | undefined\n /**\n * How this rail's records are addressed in the EC-155 reference index — the\n * target shape the delete/unpublish confirmation's inbound pre-flight queries\n * (EC-433). Absent ⇒ the document shape (`{ type: 'document', id: 'collection:id' }`),\n * which every `cmsDocuments` collection uses. The assets rail supplies\n * `{ type: 'asset' }` so its list/bulk delete confirmation enumerates the SAME\n * inbound references its detail surface does — a document-shaped lookup would\n * report a referenced asset as \"not referenced\" (the EC-319 objection that\n * originally kept `remove` off the assets rail).\n */\n referenceTarget?(ref: DocumentRef): ReferenceTarget\n /** The last failed/denied mutation, surfaced by the shell (never a silent failure). */\n readonly lastError: OperationError | null\n clearLastError(): void\n /**\n * Land any queued/optimistic writes so a subsequent pre-flight READ sees them\n * (the reference index is maintained on write). A source with no async\n * persistence omits it — its reads are already authoritative.\n */\n flush?(): Promise<void>\n\n /**\n * The studio route splat for a record's detail surface (the row link + the\n * post-create navigation target), e.g. `studioPath.cmsDocument(collectionId, id)`.\n * Rail-owned so the list never hard-codes the CMS route. `undefined` ⇒ the rail\n * has no detail surface, so rows render as plain text (no navigation).\n */\n detailPath?(id: string): string\n /**\n * The studio route splat for this rail's LIST surface — the edit shell's\n * \"back to list\" breadcrumb crumb and its post-delete navigation target, e.g.\n * `studioPath.cmsCollection(collectionId)`. Rail-owned so the edit shell never\n * hard-codes the CMS route (the counterpart to {@link detailPath}). `undefined`\n * ⇒ the crumb is unlinked and delete falls back to the project index.\n */\n readonly listPath?: string\n\n // --- View state (list) -----------------------------------------------------\n viewState(): TableViewState\n setViewState(update: TableViewState | ((prev: TableViewState) => TableViewState)): void\n\n // --- Writes ----------------------------------------------------------------\n create(): DocumentRef\n /**\n * EC-595 A2 — HOW this source's records are written: exactly one model per source,\n * so a surface can never write a revisioned document the record way or vice versa.\n * `record` — the explicit-save rails (users, redirects, assets, the account): the\n * editor's Save button hands the whole record over. `document` — the CMS: every\n * writer emits patches at its source, committed on quiescence and merged per path\n * at the store; the editor shows per-field conflicts and follows the subscription.\n */\n readonly writes: CollectionWrites\n remove(ref: DocumentRef): void\n /**\n * Bulk publish/unpublish/delete/set-field (only when `capabilities.bulk`): one\n * command PER item, per-item outcomes enumerated in the report (partial failure\n * is never silent).\n */\n bulk?(ids: readonly string[], action: BulkAction, setField?: BulkSetField): Promise<BulkReport>\n\n // --- Capability method groups (present iff the matching flag is set) --------\n /** `publish` — published-pool predicate + publish/unpublish. */\n readonly publishing?: PublishingCapability\n /**\n * Per-document timestamps (EC-445) — the data behind the `$updatedAt`\n * pseudo-column (and, once its data path lands, `$publishedAt`). Present iff the\n * source materializes them (the CMS source, sourced through session-hydration);\n * a rail that carries no timestamps omits it, so those pseudo-columns simply don't\n * apply (see {@link pseudoColumnsForSource}). The accessor shape mirrors\n * {@link PublishingCapability.isPublished} — a per-ref lookup, referentially stable.\n */\n readonly timestamps?: DocumentTimestampsCapability\n /** `versions` — version history + restore. */\n readonly versioning?: VersioningCapability\n /** `inlineEdit` — the list's single-command optimistic cell write. */\n readonly inlineEditing?: InlineEditCapability\n /**\n * Config-as-code named lenses (EC-246b) the header view selector switches\n * between. Optional: a source that declares no named views (a non-CMS rail\n * like users) omits it, and the selector renders nothing (single default).\n */\n readonly views?: NamedViewsCapability\n /**\n * `importExport` — JSON round-trip of the collection's records. CMS-specific;\n * a rail whose records are credential-bearing (users) omits it, so neither the\n * import dialog nor the export action mounts (identities aren't dumped to JSON).\n */\n readonly imports?: ImportCapability\n}\n\n/** EC-595 A2 — the write model of a source (see {@link CollectionViewSource.writes}). */\nexport type CollectionWrites = RecordWrites | DocumentWrites\n\n/**\n * An explicit-save rail: the editor's Save button writes the record. `base` (EC-616) is the\n * record as the editor OPENED it (or as of their last successful save) — a rail that can\n * write per column sends only what differs from it, so a column another writer changed\n * meanwhile is kept; absent for a record being created.\n */\nexport interface RecordWrites {\n readonly kind: 'record'\n save(doc: CmsDocument, base?: CmsDocument): void\n}\n\n/**\n * The revisioned-document write model (EC-595, ratified 2026-10-24): patches at the\n * source, the persister's queue, per-field conflicts, the document subscription.\n */\nexport interface DocumentWrites {\n readonly kind: 'document'\n /**\n * Commit the patches a writer made; resolves once acknowledged (`undefined` in a\n * fixture session). `bases` — REQUIRED, per patch: the revision of the view the writer\n * authored it against (EC-647 — never the session's current one by default; a write\n * that names none is refused).\n */\n commit(\n ref: DocumentRef,\n patches: readonly DocumentPatch[],\n bases: readonly (number | undefined)[],\n ): Promise<CommitOutcome | undefined>\n /** The revision the session's view of a document is at now. */\n revisionOf(ref: DocumentRef): number | undefined\n /**\n * EC-647 — the revision a document value the session SHOWED is at (`undefined` for a\n * value it never showed, or a provisional one): a writer holding the value it displayed\n * holds its revision. EC-667 — a view showing a pending write is a composite: with\n * `path`, the revision the content at that position was authored against (an editor's\n * first keystroke there); without it, the EARLIEST of any position the view shows — the\n * token of a whole-document writer.\n */\n revisionOfView(view: CmsDocument, path?: ValuePath): number | undefined\n /** Create a document from a whole value (insert-if-absent): duplicate. */\n insert(doc: CmsDocument): void\n /** A fresh document id from the session's injected factory (EC-578). */\n newId(): string\n /** The open per-field conflicts on a document. */\n conflicts(ref: DocumentRef): readonly DocumentConflict[]\n resolveConflict(ref: DocumentRef, conflictId: string, choice: 'theirs' | 'mine'): void\n /** Adopt a row the document subscription pushed (local edits win on their paths until flushed). */\n adoptSnapshot(ref: DocumentRef, record: CmsDocumentRecord | undefined): void\n /** The forced flush's persistence half: await every queued write of the document. */\n flush(ref: DocumentRef): Promise<void>\n /**\n * Write the whole document at `revision` — the revision of the view the snapshot was\n * authored on (EC-647; a document-level key the value-position grammar has no position\n * for — the slot `alias`). A move since is refused, never overwritten. `authoredOn`: the\n * document the snapshot was made from — a refusal is re-sent per position against it\n * (EC-686).\n */\n replace(doc: CmsDocument, revision: number | undefined, authoredOn?: CmsDocument): void\n /**\n * EC-595 point 7 — follow ONE document while it is open: every pushed row is adopted\n * under the local edits (the editor rebases), and the presence on it is delivered to\n * `onPresence`. Returns the unsubscribe function.\n */\n subscribe(ref: DocumentRef, onPresence: (presence: readonly PresenceEntry[]) => void): () => void\n /** Thin presence (Stage A): this session is on the document, in `path`'s field (or on no field). */\n reportPresence(ref: DocumentRef, path: ValuePath | undefined): void\n /** This session left the document. */\n clearPresence(ref: DocumentRef): void\n /** This session's presence id (one per tab), so the editor can leave itself out. */\n readonly presenceSessionId: string\n}\n\n/**\n * The capability matrix. Each flag gates a shell region AND the presence of its\n * method group. The **users rail** (EC-240) sets `publish:false`, `versions:false`\n * (credential caution), `create:false` (identity minted by sign-up), and no\n * `liveUrl`/`hierarchy`.\n */\nexport interface CollectionViewCapabilities {\n readonly publish: boolean\n readonly versions: boolean\n readonly locales: boolean\n readonly inlineEdit: boolean\n readonly create: boolean\n /**\n * Duplicate-document action (EC-442, amended same day). Deliberately its own\n * flag, never derived from `create`: the owner call (2026-07-17) is CMS\n * collections ONLY — no primitive rail offers Duplicate for now (identities\n * and uploads aren't copyable content; redirects deferred until requested as\n * a product feature).\n */\n readonly duplicate: boolean\n readonly remove: boolean\n readonly bulk: boolean\n /** The derived live-URL column (published pool × deployment routes). */\n readonly liveUrl: boolean\n /** Parent-self-relation tree mode (hierarchical collections only). */\n readonly hierarchy: boolean\n /** JSON import/export of the collection's records (off for credential rails). */\n readonly importExport: boolean\n /**\n * The rail is administration-scoped (mirrors `ADMIN_ONLY_OPERATIONS`). The\n * nav-permission layer reads this to gate the rail item. Absent ⇒ not admin-only.\n */\n readonly adminOnly?: boolean\n}\n\n/**\n * Principal-based field access (EC-240, AD-18). One tri-state drives BOTH visibility\n * and writability: `edit` (writable), `read` (visible, read-only), `hidden` (not\n * rendered). Default `edit` (default-open). Distinct from content-`hidden(values)`;\n * they compose — a field shows iff *not content-hidden AND access ≠ hidden*, and is\n * editable iff *access = edit AND not otherwise read-only* (published view, `form.readOnly`).\n */\nexport type FieldAccess = 'edit' | 'read' | 'hidden'\n\n/**\n * Resolve a field's effective principal access for a source — its {@link\n * CollectionViewSource.fieldAccess} policy when present, else derived from the\n * boolean `isFieldReadOnly` (default-open). The one place the shell asks \"may this\n * principal edit / see this field?\", so the default-open fallback lives in one spot.\n */\nexport function resolveFieldAccess(\n source: CollectionViewSource,\n field: FieldDef,\n record?: CmsDocument,\n): FieldAccess {\n return source.fieldAccess?.(field, record) ?? (source.isFieldReadOnly(field) ? 'read' : 'edit')\n}\n\n/**\n * Asset display metadata a cell/field renders (thumbnail or labelled badge). EC-587:\n * named apart from core's `ResolvedAsset` (the delivery shape) — this is the studio's\n * label/thumbnail slice; its `alt` is the one reader's (`assetAlt`).\n */\nexport interface AssetDisplay {\n readonly url?: string\n readonly contentType?: string\n readonly title?: string\n readonly filename?: string\n readonly alt?: string\n}\n\n/** `publish` capability. */\nexport interface PublishingCapability {\n /** Published-ness from the version pointer (NOT a row field). */\n isPublished(ref: DocumentRef): boolean\n /**\n * Does this PUBLISHED document's working draft carry unpublished changes\n * (EC-449)? The cheap per-row signal behind the amber \"changed\" `$state` dot —\n * sourced from the MATERIALIZED `hasUnpublishedChanges` record flag (computed\n * once at the write chokepoint), never a per-row snapshot compare. Optional: a\n * source without the signal omits it, and the list derives only\n * published/draft (exactly the pre-EC-449 rendering). Same referentially-stable\n * per-ref accessor shape as {@link isPublished}.\n */\n hasUnpublishedChanges?(ref: DocumentRef): boolean\n publish(ref: DocumentRef): Promise<void>\n unpublish(ref: DocumentRef): Promise<void>\n /**\n * Live URLs for the routable documents, keyed by id — derived from the\n * PUBLISHED pool (never draft rows) so an unpublished slug/parent edit can't\n * leak into a live link (EC-205/EC-216). `null` when the collection is not\n * routable → the live-URL column is hidden. Only present when `capabilities.liveUrl`.\n *\n * EC-628: a {@link ReadState} — the URL map only when the published read answered; a\n * failed read is `error` with its sentence, which the URL column shows in every row\n * (never the \"not published\" dash an empty map would give).\n */\n liveUrls?(): ReadState<ReadonlyMap<string, DocumentUrl>> | null\n}\n\n/**\n * `timestamps` capability (EC-445/EC-447): per-document record timestamps as ISO\n * strings, or `undefined` when a document carries none. The `$updatedAt`/`$publishedAt`\n * pseudo-columns read + sort by these. `updatedAt` is the last-write instant;\n * `publishedAt` (EC-447) is the FIRST-publish instant — set-once on publish, cleared on\n * unpublish, so it is `undefined` for a draft / never-published / currently-unpublished\n * document (the column shows EmDash).\n */\nexport interface DocumentTimestampsCapability {\n updatedAt(ref: DocumentRef): string | undefined\n publishedAt(ref: DocumentRef): string | undefined\n}\n\n/**\n * The reserved pseudo-columns applicable to a source's list (EC-445), derived from\n * its capabilities: `$updatedAt` iff it materializes timestamps, `$state` iff it has a\n * publish lifecycle, `$url` iff it resolves delivery URLs. The one place the list\n * surface + the Display picker agree on which pseudo-columns exist for a source, so\n * the column universe never drifts between where columns render and where they toggle.\n */\nexport function pseudoColumnsForSource(source: CollectionViewSource): string[] {\n return pseudoColumns(source.schema, {\n timestamps: source.timestamps !== undefined,\n publishing: source.publishing !== undefined,\n routable: source.capabilities.liveUrl,\n })\n}\n\n/** `versions` capability. */\nexport interface VersioningCapability {\n restore(ref: DocumentRef, version: number, scope?: RestoreScope): Promise<CmsDocument | null>\n /**\n * The document's recorded version history + live pin. Folds the flush-before-read\n * ordering INTO the loader (the adapter's concern), so every fetch lists an\n * authoritative history regardless of which surface triggered the change (EC-224).\n */\n load(ref: DocumentRef): Promise<DocumentVersionsView>\n}\n\n/** `inlineEdit` capability: the list's single-command optimistic cell write (EC-168). */\nexport interface InlineEditCapability {\n commitField(input: CellCommitInput): Promise<CellCommitOutcome>\n}\n\n/** A named lens the header selector offers — id + display name only (the header never needs its state). */\nexport interface NamedView {\n readonly id: string\n /** Display name; a {@link LocalizedString} (EC-343), resolved at the render site like `form.label`. */\n readonly name: LocalizedString\n}\n\n/** `importExport` capability: validate + fold external JSON records into the collection (EC-079). */\nexport interface ImportCapability {\n importDocuments(items: unknown[]): ImportReport\n}\n\n/**\n * `views` capability: config-as-code named lenses (EC-246b). The header selector\n * only *picks* a lens — it never creates/edits/deletes one (shared views are code).\n */\nexport interface NamedViewsCapability {\n /** The built-in default + config-declared named views. */\n list(): readonly NamedView[]\n /** The currently-selected view id. */\n activeId(): string\n /** Switch views (resets the working table state to the view's baseline). */\n setActive(id: string): void\n}\n\nexport type { CellCommitInput, CellCommitOutcome, ImportReport }\n"]}
@@ -13,4 +13,4 @@
13
13
  * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
14
14
  * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
15
15
  */
16
- export declare const ENGINE_VERSION = "0.0.98";
16
+ export declare const ENGINE_VERSION = "0.0.99";
@@ -13,5 +13,5 @@
13
13
  * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the
14
14
  * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)
15
15
  */
16
- export const ENGINE_VERSION = '0.0.98';
16
+ export const ENGINE_VERSION = '0.0.99';
17
17
  //# sourceMappingURL=engine-version.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.98'\n"]}
1
+ {"version":3,"file":"engine-version.js","sourceRoot":"","sources":["../../src/lib/engine-version.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,QAAQ,CAAA","sourcesContent":["/**\n * The engine version this studio build was compiled from (EC-552) — the client-side\n * half of the deployed-functions version handshake: the boot gate compares it against\n * what `delivery:engineVersion` answers from the deployment.\n *\n * A MIRROR of `package.json`'s `version`, not the declaration itself: client source\n * cannot import `../../package.json` (outside the tsc build's `rootDir`; moving the\n * root would reshape `dist` and every export path). The parity test beside this file\n * fs-reads the real declaration and fails `pnpm verify` the moment the two diverge,\n * so a release cannot ship with a stale mirror. `pnpm release:bump` writes this\n * alongside the five `package.json` versions (EC-608).\n *\n * (The convex-side stamp does NOT read this mirror — `convex/delivery.ts` imports the\n * package.json declaration directly; its bundler allows what tsc's rootDir forbids.)\n */\nexport const ENGINE_VERSION = '0.0.99'\n"]}
@@ -88,7 +88,20 @@ export const deMessages = {
88
88
  'shell.nav.planned': 'Geplant',
89
89
  'shell.nav.collapseView': '{name} einklappen',
90
90
  'shell.nav.expandView': '{name} ausklappen',
91
- 'shell.nav.issuesCount': '{count} Sidebar-Probleme',
91
+ 'shell.notices.title': 'Probleme',
92
+ 'shell.notices.open': 'Probleme anzeigen: {summary}',
93
+ 'shell.notices.none': 'Keine Probleme',
94
+ 'shell.notices.errors.one': '{count} Fehler',
95
+ 'shell.notices.errors.other': '{count} Fehler',
96
+ 'shell.notices.warnings.one': '{count} Warnung',
97
+ 'shell.notices.warnings.other': '{count} Warnungen',
98
+ 'shell.notices.empty': 'Gerade ist nichts zu tun.',
99
+ 'shell.notices.goTo': 'Öffnen',
100
+ 'shell.notices.dismiss': 'Schließen',
101
+ 'shell.notices.declaredInProject': 'in {file}',
102
+ 'shell.notices.declaredInStudio': 'in der defineStudio({…})-Konfiguration',
103
+ 'shell.notices.deploymentSettings': 'Convex-Deployment: {settings}',
104
+ 'shell.notices.reservedSource': 'Ein Addon meldet Befunde unter „{source}“, einem Quellnamen, den die Engine reserviert. Benenne die Quelle um, die es an useNavIssues übergibt.',
92
105
  // EC-606 — gespeicherte Dokumente einer Sammlung, die das Projekt nicht (mehr) deklariert.
93
106
  'shell.nav.orphanedCollections': 'Gespeicherte Dokumente aus Sammlungen, die dieses Projekt nicht deklariert, sind im Studio ausgeblendet: {collections}. Um sie zurückzuholen, deklarieren Sie die Sammlung in „{file}“ erneut (oder stellen Sie ihre alte id wieder her).',
94
107
  'shell.nav.orphanedCollections.item': '„{collection}“ ({count} gespeichert)',
@@ -145,7 +158,9 @@ export const deMessages = {
145
158
  'shell.devLogin.hint': 'Automatische Dev-Anmeldung: Das Studio wurde über VITE_ELYTRA_DEV_LOGIN automatisch angemeldet (nur in Entwicklungs-Builds). Entfernen Sie die Variable, um die echte Anmeldung zu testen.',
146
159
  'shell.playground.badge': 'Playground',
147
160
  'shell.playground.hint': 'Playground-Modus: Daten liegen im Arbeitsspeicher dieses Geräts und werden beim Neuladen zurückgesetzt. Es wird nichts in einem Backend gespeichert.',
148
- 'shell.deliveryHealth.dismiss': 'Schließen',
161
+ 'shell.deliveryHealth.title': 'Sofort-Veröffentlichung nicht eingerichtet',
162
+ 'shell.deliveryHealth.missing': '{vars} auf diesem Convex-Deployment nicht gesetzt. Veröffentlichen funktioniert, aber die Live-Site aktualisiert sich erst, wenn sie gesetzt sind (siehe CONNECT.md) und die Functions erneut gepusht wurden. Ein Projekt ohne Revalidate-Host deklariert instantPublish: false in {file}.',
163
+ 'shell.deliveryHealth.unchecked': 'Sofort-Veröffentlichung konnte nicht geprüft werden: {error}',
149
164
  'shell.rolePolicyDrift.title': 'Diese Deployment-Umgebung setzt eine andere Rollenrichtlinie durch, als das Projekt deklariert – Berechtigungen sind möglicherweise nicht wie erwartet.',
150
165
  // ─────────────────────────────────────────────────────────────────────────
151
166
  // Customization issues — the section-rail foot (`NavIssues`)
@@ -571,6 +586,7 @@ export const deMessages = {
571
586
  'cms.conflict.empty': '(leer)',
572
587
  'cms.conflict.structured': 'ein strukturierter Wert',
573
588
  'cms.conflict.cellRefused': 'Dieses Feld wurde seit dem Laden von jemand anderem geändert — dessen Wert wird angezeigt.',
589
+ 'cms.conflict.aliasRefused': 'Der Slot wurde von jemand anderem geändert, seit Sie ihn gewählt haben — dessen Wahl bleibt. Wählen Sie ihn erneut, um ihn zu ändern.',
574
590
  'cms.conflict.unauthored': 'Eine Änderung wurde nicht gespeichert: Sie gibt nicht an, auf welcher Fassung des Dokuments sie beruht, und könnte eine neuere Änderung überschreiben. Laden Sie das Dokument neu und nehmen Sie die Änderung erneut vor.',
575
591
  'cms.presence.inField': '{name} ist in diesem Feld',
576
592
  'cms.presence.inFieldMany': '{names} sind in diesem Feld',
@@ -606,7 +622,7 @@ export const deMessages = {
606
622
  'cms.issueMessage.unknownVersion': 'Für das Dokument „{id}“ ist keine Version {version} aufgezeichnet.',
607
623
  'cms.issueMessage.routeConflict': 'Die Routen „{first}“ und „{second}“ treffen auf dieselben URLs zu (Muster „{pattern}“).',
608
624
  'cms.issueMessage.redirectLoop': 'Die Weiterleitungskette ab „{from}“ bildet eine Schleife (…{chain}).',
609
- 'cms.issueMessage.duplicateRedirect': 'Die Weiterleitung „{redirect}“ (von „{redirectFrom}“) doppelt die Weiterleitung „{kept}“ (von „{keptFrom}“) — ohne abschließende Schrägstriche sind beide „{from}“. „{kept}“ gilt; lösche oder ändere die andere.',
625
+ 'cms.issueMessage.duplicateRedirect': 'Die Weiterleitung von „{redirectFrom}“ doppelt die Weiterleitung von „{keptFrom}“ — ohne abschließende Schrägstriche sind beide „{from}“. Die Weiterleitung von „{keptFrom}“ gilt; lösche oder ändere diese.',
610
626
  'cms.issueMessage.redirectUnreadable': 'Die gespeicherte Weiterleitung {redirect} konnte nicht gelesen werden und wird nicht ausgeliefert ({reason}).',
611
627
  'cms.issueMessage.redirectReportUnreadable': 'Das Deployment meldete das Weiterleitungsproblem #{index}, das diese Website nicht lesen kann — eine Weiterleitung wird nicht ausgeliefert; betreibe die Website mit der Engine-Version des Deployments.',
612
628
  'cms.issueMessage.allDynamicShadowsMount': 'Die Route „{route}“ („{pattern}“) besteht nur aus dynamischen Segmenten und trifft damit auf JEDE URL mit {depth} Segmenten zu — die Hierarchie-Einhängung „{mountRoute}“ („{mountPattern}“) wird auf dieser Ebene nur dort herangezogen, wo die Route kein Dokument auflöst. Nichts ist dadurch grundsätzlich unerreichbar, aber der Vorrang entscheidet sich dort am INHALT statt an den Routen: {shadowed} werden stillschweigend verdeckt, sobald ein zusammengesetzter Pfad mit {claimed} kollidiert — und keine Seite erfährt davon. Geben Sie der Route ein statisches Präfix („/blog/:slug“), damit sie einen Namensraum beansprucht statt einer ganzen Ebene.',
@@ -778,6 +794,8 @@ export const deMessages = {
778
794
  'cms.field.slug.regenerate': 'Slug neu erzeugen',
779
795
  'cms.field.slug.taken': 'Wird bereits von „{title}“ verwendet',
780
796
  'cms.field.slug.takenGeneric': 'Dieser Slug wird bereits verwendet.',
797
+ // EC-691 — a page an explicit route binds answers only at that route's address.
798
+ 'cms.field.slug.routedComposesChildren': 'Die Adresse dieser Seite ist {path}. Ihr Slug bildet nur die Pfade ihrer Unterseiten.',
781
799
  // ─────────────────────────────────────────────────────────────────────────
782
800
  // CMS — relation picker
783
801
  // ─────────────────────────────────────────────────────────────────────────