@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,61 @@
1
+ /**
2
+ * EC-690 — the TRANSIENT channel of the studio's one notification-and-issue surface: an
3
+ * action's outcome (a publish that failed, a copy that was made). Persistent findings (a
4
+ * duplicate redirect, a config problem) live in the shell's findings registry instead
5
+ * (`components/nav-issues-registry.tsx`); both render in the one issues tray.
6
+ *
7
+ * The rules, owner-ratified 2026-10-28 ("As recommended", ruling (d)):
8
+ * - a SUCCESS expires on its own after {@link NOTICE_PEEK_MS};
9
+ * - an ERROR stays until it is dismissed — it is never lost to a timer;
10
+ * - every notice PEEKS for {@link NOTICE_PEEK_MS} when it is raised (bottom-right of the
11
+ * content area), and lives on in the tray for as long as it lives at all.
12
+ *
13
+ * A KEYED notice is an outcome its owner tracks (a failed save its store holds as
14
+ * `persistError` / `lastError`): the owner raises it while the error is set and retracts it
15
+ * when the error clears, and dismissing it in the tray calls the owner's `onDismiss` — so the
16
+ * inline banner and the tray are two views of ONE state, never two states.
17
+ *
18
+ * Pure and clock-injected ({@link createToastStore}): the module's default store binds the
19
+ * real clock, tests bind their own (AGENTS §4 determinism — no `Date.now()`/`setTimeout` in
20
+ * the logic under test).
21
+ */
22
+ export type ToastVariant = 'error' | 'success';
23
+ export interface ToastItem {
24
+ id: number;
25
+ message: string;
26
+ variant: ToastVariant;
27
+ /** When the notice leaves on its own (epoch ms); `null` = it stays until dismissed (every error). */
28
+ expiresAt: number | null;
29
+ /** Whether it is still in its first {@link NOTICE_PEEK_MS} (shown in the content area's corner). */
30
+ peeking: boolean;
31
+ /** The owner's key, for a notice its owner raises and retracts (absent for a plain `toast.*`). */
32
+ key?: string;
33
+ }
34
+ /** How long a notice peeks, and how long a success lives (EC-268's original 5 s). */
35
+ export declare const NOTICE_PEEK_MS = 5000;
36
+ /** The clock the store reads — injected so the logic is deterministic under test. */
37
+ export interface ToastClock {
38
+ now(): number;
39
+ /** Run `run` after `ms`; returns a cancel function. */
40
+ schedule(run: () => void, ms: number): () => void;
41
+ }
42
+ export interface KeyedToastInput {
43
+ message: string;
44
+ variant: ToastVariant;
45
+ /** Called when the reader dismisses it (e.g. `clearPersistError`) — not on {@link ToastStore.retract}. */
46
+ onDismiss?: () => void;
47
+ }
48
+ export interface ToastStore {
49
+ push(message: string, variant: ToastVariant): number;
50
+ /** Raise (or keep) the owner's notice under `key`. The same message again is a no-op; a new one replaces it. */
51
+ raise(key: string, input: KeyedToastInput): number;
52
+ /** The owner's state cleared: remove its notice WITHOUT calling `onDismiss`. */
53
+ retract(key: string): void;
54
+ /** The reader dismissed it: remove it and tell its owner. */
55
+ dismiss(id: number): void;
56
+ getSnapshot(): ToastItem[];
57
+ subscribe(listener: (items: ToastItem[]) => void): () => void;
58
+ }
59
+ export declare function createToastStore(clock: ToastClock): ToastStore;
60
+ /** The studio's one transient channel (`toast.*` writes it, the issues tray reads it). */
61
+ export declare const studioToasts: ToastStore;
@@ -0,0 +1,116 @@
1
+ /**
2
+ * EC-690 — the TRANSIENT channel of the studio's one notification-and-issue surface: an
3
+ * action's outcome (a publish that failed, a copy that was made). Persistent findings (a
4
+ * duplicate redirect, a config problem) live in the shell's findings registry instead
5
+ * (`components/nav-issues-registry.tsx`); both render in the one issues tray.
6
+ *
7
+ * The rules, owner-ratified 2026-10-28 ("As recommended", ruling (d)):
8
+ * - a SUCCESS expires on its own after {@link NOTICE_PEEK_MS};
9
+ * - an ERROR stays until it is dismissed — it is never lost to a timer;
10
+ * - every notice PEEKS for {@link NOTICE_PEEK_MS} when it is raised (bottom-right of the
11
+ * content area), and lives on in the tray for as long as it lives at all.
12
+ *
13
+ * A KEYED notice is an outcome its owner tracks (a failed save its store holds as
14
+ * `persistError` / `lastError`): the owner raises it while the error is set and retracts it
15
+ * when the error clears, and dismissing it in the tray calls the owner's `onDismiss` — so the
16
+ * inline banner and the tray are two views of ONE state, never two states.
17
+ *
18
+ * Pure and clock-injected ({@link createToastStore}): the module's default store binds the
19
+ * real clock, tests bind their own (AGENTS §4 determinism — no `Date.now()`/`setTimeout` in
20
+ * the logic under test).
21
+ */
22
+ /** How long a notice peeks, and how long a success lives (EC-268's original 5 s). */
23
+ export const NOTICE_PEEK_MS = 5000;
24
+ export function createToastStore(clock) {
25
+ let items = [];
26
+ let seq = 0;
27
+ const listeners = new Set();
28
+ const cancels = new Map();
29
+ const onDismiss = new Map();
30
+ const emit = () => {
31
+ for (const listener of [...listeners])
32
+ listener(items);
33
+ };
34
+ const drop = (id) => {
35
+ const item = items.find((candidate) => candidate.id === id);
36
+ if (!item)
37
+ return undefined;
38
+ items = items.filter((candidate) => candidate.id !== id);
39
+ cancels.get(id)?.();
40
+ cancels.delete(id);
41
+ return item;
42
+ };
43
+ const add = (message, variant, key) => {
44
+ const id = ++seq;
45
+ const raisedAt = clock.now();
46
+ const expiresAt = variant === 'success' ? raisedAt + NOTICE_PEEK_MS : null;
47
+ items = [...items, { id, message, variant, expiresAt, peeking: true, ...(key !== undefined ? { key } : {}) }];
48
+ cancels.set(id, clock.schedule(() => {
49
+ cancels.delete(id);
50
+ if (variant === 'success') {
51
+ onDismiss.delete(id);
52
+ items = items.filter((candidate) => candidate.id !== id);
53
+ }
54
+ else {
55
+ items = items.map((candidate) => (candidate.id === id ? { ...candidate, peeking: false } : candidate));
56
+ }
57
+ emit();
58
+ }, NOTICE_PEEK_MS));
59
+ emit();
60
+ return id;
61
+ };
62
+ return {
63
+ push: (message, variant) => add(message, variant),
64
+ raise(key, input) {
65
+ const current = items.find((item) => item.key === key);
66
+ if (current && current.message === input.message && current.variant === input.variant) {
67
+ if (input.onDismiss)
68
+ onDismiss.set(current.id, input.onDismiss);
69
+ return current.id;
70
+ }
71
+ if (current) {
72
+ drop(current.id);
73
+ onDismiss.delete(current.id);
74
+ }
75
+ const id = add(input.message, input.variant, key);
76
+ if (input.onDismiss)
77
+ onDismiss.set(id, input.onDismiss);
78
+ return id;
79
+ },
80
+ retract(key) {
81
+ const current = items.find((item) => item.key === key);
82
+ if (!current)
83
+ return;
84
+ drop(current.id);
85
+ onDismiss.delete(current.id);
86
+ emit();
87
+ },
88
+ dismiss(id) {
89
+ const item = drop(id);
90
+ if (!item)
91
+ return;
92
+ const owner = onDismiss.get(id);
93
+ onDismiss.delete(id);
94
+ emit();
95
+ owner?.();
96
+ },
97
+ getSnapshot: () => items,
98
+ subscribe(listener) {
99
+ listeners.add(listener);
100
+ return () => {
101
+ listeners.delete(listener);
102
+ };
103
+ },
104
+ };
105
+ }
106
+ /** The real clock — the one place the studio's notices read wall time. */
107
+ const realClock = {
108
+ now: () => Date.now(),
109
+ schedule: (run, ms) => {
110
+ const handle = setTimeout(run, ms);
111
+ return () => clearTimeout(handle);
112
+ },
113
+ };
114
+ /** The studio's one transient channel (`toast.*` writes it, the issues tray reads it). */
115
+ export const studioToasts = createToastStore(realClock);
116
+ //# sourceMappingURL=toast-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"toast-store.js","sourceRoot":"","sources":["../../../src/lib/notices/toast-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * EC-690 — the TRANSIENT channel of the studio's one notification-and-issue surface: an\n * action's outcome (a publish that failed, a copy that was made). Persistent findings (a\n * duplicate redirect, a config problem) live in the shell's findings registry instead\n * (`components/nav-issues-registry.tsx`); both render in the one issues tray.\n *\n * The rules, owner-ratified 2026-10-28 (\"As recommended\", ruling (d)):\n * - a SUCCESS expires on its own after {@link NOTICE_PEEK_MS};\n * - an ERROR stays until it is dismissed — it is never lost to a timer;\n * - every notice PEEKS for {@link NOTICE_PEEK_MS} when it is raised (bottom-right of the\n * content area), and lives on in the tray for as long as it lives at all.\n *\n * A KEYED notice is an outcome its owner tracks (a failed save its store holds as\n * `persistError` / `lastError`): the owner raises it while the error is set and retracts it\n * when the error clears, and dismissing it in the tray calls the owner's `onDismiss` — so the\n * inline banner and the tray are two views of ONE state, never two states.\n *\n * Pure and clock-injected ({@link createToastStore}): the module's default store binds the\n * real clock, tests bind their own (AGENTS §4 determinism — no `Date.now()`/`setTimeout` in\n * the logic under test).\n */\n\nexport type ToastVariant = 'error' | 'success'\n\nexport interface ToastItem {\n id: number\n message: string\n variant: ToastVariant\n /** When the notice leaves on its own (epoch ms); `null` = it stays until dismissed (every error). */\n expiresAt: number | null\n /** Whether it is still in its first {@link NOTICE_PEEK_MS} (shown in the content area's corner). */\n peeking: boolean\n /** The owner's key, for a notice its owner raises and retracts (absent for a plain `toast.*`). */\n key?: string\n}\n\n/** How long a notice peeks, and how long a success lives (EC-268's original 5 s). */\nexport const NOTICE_PEEK_MS = 5000\n\n/** The clock the store reads — injected so the logic is deterministic under test. */\nexport interface ToastClock {\n now(): number\n /** Run `run` after `ms`; returns a cancel function. */\n schedule(run: () => void, ms: number): () => void\n}\n\nexport interface KeyedToastInput {\n message: string\n variant: ToastVariant\n /** Called when the reader dismisses it (e.g. `clearPersistError`) — not on {@link ToastStore.retract}. */\n onDismiss?: () => void\n}\n\nexport interface ToastStore {\n push(message: string, variant: ToastVariant): number\n /** Raise (or keep) the owner's notice under `key`. The same message again is a no-op; a new one replaces it. */\n raise(key: string, input: KeyedToastInput): number\n /** The owner's state cleared: remove its notice WITHOUT calling `onDismiss`. */\n retract(key: string): void\n /** The reader dismissed it: remove it and tell its owner. */\n dismiss(id: number): void\n getSnapshot(): ToastItem[]\n subscribe(listener: (items: ToastItem[]) => void): () => void\n}\n\nexport function createToastStore(clock: ToastClock): ToastStore {\n let items: ToastItem[] = []\n let seq = 0\n const listeners = new Set<(items: ToastItem[]) => void>()\n const cancels = new Map<number, () => void>()\n const onDismiss = new Map<number, () => void>()\n\n const emit = (): void => {\n for (const listener of [...listeners]) listener(items)\n }\n\n const drop = (id: number): ToastItem | undefined => {\n const item = items.find((candidate) => candidate.id === id)\n if (!item) return undefined\n items = items.filter((candidate) => candidate.id !== id)\n cancels.get(id)?.()\n cancels.delete(id)\n return item\n }\n\n const add = (message: string, variant: ToastVariant, key?: string): number => {\n const id = ++seq\n const raisedAt = clock.now()\n const expiresAt = variant === 'success' ? raisedAt + NOTICE_PEEK_MS : null\n items = [...items, { id, message, variant, expiresAt, peeking: true, ...(key !== undefined ? { key } : {}) }]\n cancels.set(\n id,\n clock.schedule(() => {\n cancels.delete(id)\n if (variant === 'success') {\n onDismiss.delete(id)\n items = items.filter((candidate) => candidate.id !== id)\n } else {\n items = items.map((candidate) => (candidate.id === id ? { ...candidate, peeking: false } : candidate))\n }\n emit()\n }, NOTICE_PEEK_MS),\n )\n emit()\n return id\n }\n\n return {\n push: (message, variant) => add(message, variant),\n raise(key, input) {\n const current = items.find((item) => item.key === key)\n if (current && current.message === input.message && current.variant === input.variant) {\n if (input.onDismiss) onDismiss.set(current.id, input.onDismiss)\n return current.id\n }\n if (current) {\n drop(current.id)\n onDismiss.delete(current.id)\n }\n const id = add(input.message, input.variant, key)\n if (input.onDismiss) onDismiss.set(id, input.onDismiss)\n return id\n },\n retract(key) {\n const current = items.find((item) => item.key === key)\n if (!current) return\n drop(current.id)\n onDismiss.delete(current.id)\n emit()\n },\n dismiss(id) {\n const item = drop(id)\n if (!item) return\n const owner = onDismiss.get(id)\n onDismiss.delete(id)\n emit()\n owner?.()\n },\n getSnapshot: () => items,\n subscribe(listener) {\n listeners.add(listener)\n return () => {\n listeners.delete(listener)\n }\n },\n }\n}\n\n/** The real clock — the one place the studio's notices read wall time. */\nconst realClock: ToastClock = {\n now: () => Date.now(),\n schedule: (run, ms) => {\n const handle = setTimeout(run, ms)\n return () => clearTimeout(handle)\n },\n}\n\n/** The studio's one transient channel (`toast.*` writes it, the issues tray reads it). */\nexport const studioToasts: ToastStore = createToastStore(realClock)\n"]}
@@ -44,7 +44,12 @@ import type { OperationError, OperationsClient } from './operations-package.js';
44
44
  * the base it was authored at. `revisionOfView(view, path)` is the revision the content
45
45
  * at `path` was authored against: the earliest of the floor and of the shown writes that
46
46
  * OVERLAP `path` — at it, at an ancestor, or at a descendant, whose content the value at
47
- * `path` contains (EC-681, Codex F23); a sibling's write does not lower it.
47
+ * `path` contains (EC-681, Codex F23); a sibling's write does not lower it. Positions
48
+ * are compared in their STRUCTURAL form, as the store compares them (EC-693, Codex F28):
49
+ * a node path carries its component ancestry — each shown write's as the merge applied
50
+ * it in that view, the asked path's expanded against the view — so a parent node's token
51
+ * sees a pending write of its child's prop, which a submitted `nodePropPath` names
52
+ * without the parent.
48
53
  * `revisionOfView(view)` — a whole-document writer — the EARLIEST of all. The record is
49
54
  * immutable, so it outlives the pending write: an edit started on a view that showed a
50
55
  * snapshot keeps the snapshot's revision after the snapshot is refused or fails
@@ -59,7 +64,8 @@ import type { OperationError, OperationsClient } from './operations-package.js';
59
64
  * - **The own-write ledger.** A base may be LIFTED, at send time, across revisions this
60
65
  * persister wrote — and only those: across a run of consecutive own revisions right
61
66
  * after it (nothing foreign happened in between), or to an own write's revision when
62
- * the patch only touches positions that write covered and the write was sent at or
67
+ * the patch only touches positions that write covered (both read where they land in the
68
+ * server row, node ancestry included — EC-693) and the write was sent at or
63
69
  * below the patch's base (the store refused every foreign change of those positions in
64
70
  * between, or the write would not have applied) — or, EC-681, across a revision someone
65
71
  * else wrote that the patch log shows touched none of the patch's positions (the store
@@ -112,7 +118,13 @@ export type CommitOutcome = {
112
118
  readonly status: 'refused';
113
119
  readonly conflicts: readonly DocumentConflict[];
114
120
  }
115
- /** A transport / authorization failure, or a write that named no authoring revision (also reported through `onError`). */
121
+ /**
122
+ * A transport / authorization failure, or a write that named no authoring revision (also
123
+ * reported through `onError`). A write the backend did not judge is KEPT (EC-674): it
124
+ * stays shown and queued, and the next commit or forced flush re-sends it — its commit,
125
+ * and every commit queued behind it, settles `failed` with that error when the drain
126
+ * stops on it (EC-686), never left waiting.
127
+ */
116
128
  | {
117
129
  readonly status: 'failed';
118
130
  readonly error: OperationError;
@@ -170,8 +182,16 @@ export interface CmsPersister {
170
182
  * conflict, never overwritten. `revision` `undefined`: the snapshot was authored on a
171
183
  * provisional view (our own pending create; insert-if-absent when untracked). Sent in
172
184
  * its place among the session's queued patches.
185
+ *
186
+ * EC-686 (the owner's ruling, per-position): when the store refuses it, it is decomposed
187
+ * into what the author changed against `authoredOn` — the document the snapshot was made
188
+ * from — and each position is re-sent: it lands, or becomes that field's ordinary
189
+ * conflict. Never a whole-document keep-mine. Without `authoredOn` nothing can be judged
190
+ * per position: the refusal adopts the current row and is reported, as before — never a
191
+ * guess (the session's view at that moment may hold changes the snapshot never saw, which
192
+ * a diff against it would re-send as the author's).
173
193
  */
174
- replace(document: CmsDocument, revision: number | undefined): void;
194
+ replace(document: CmsDocument, revision: number | undefined, authoredOn?: CmsDocument): void;
175
195
  deleteDocument(ref: DocumentRef): void;
176
196
  unpublishDocument(ref: DocumentRef): void;
177
197
  /**