@elytracms/studio 0.0.78 → 0.0.79
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.
- package/dist/components/app-views/default-modules.js +5 -1
- package/dist/components/app-views/default-modules.js.map +1 -1
- package/dist/components/assets/asset-thumb.js +12 -11
- package/dist/components/assets/asset-thumb.js.map +1 -1
- package/dist/components/builder/blocks-panel.d.ts +11 -5
- package/dist/components/builder/blocks-panel.js +18 -24
- package/dist/components/builder/blocks-panel.js.map +1 -1
- package/dist/components/builder/canvas-render-context.d.ts +22 -0
- package/dist/components/builder/canvas-render-context.js +65 -9
- package/dist/components/builder/canvas-render-context.js.map +1 -1
- package/dist/components/builder/reuse.d.ts +25 -0
- package/dist/components/builder/reuse.js +11 -2
- package/dist/components/builder/reuse.js.map +1 -1
- package/dist/components/cms/asset-field.js +52 -19
- package/dist/components/cms/asset-field.js.map +1 -1
- package/dist/components/cms/asset-picker-core.d.ts +40 -0
- package/dist/components/cms/asset-picker-core.js +21 -0
- package/dist/components/cms/asset-picker-core.js.map +1 -1
- package/dist/components/cms/asset-picker.d.ts +11 -2
- package/dist/components/cms/asset-picker.js +34 -13
- package/dist/components/cms/asset-picker.js.map +1 -1
- package/dist/components/cms/cell.js +2 -1
- package/dist/components/cms/cell.js.map +1 -1
- package/dist/components/cms/cms-store.d.ts +8 -1
- package/dist/components/cms/cms-store.js +8 -7
- package/dist/components/cms/cms-store.js.map +1 -1
- package/dist/components/cms/collection-table.js +7 -1
- package/dist/components/cms/collection-table.js.map +1 -1
- package/dist/components/cms/date-input-core.d.ts +64 -0
- package/dist/components/cms/date-input-core.js +78 -0
- package/dist/components/cms/date-input-core.js.map +1 -0
- package/dist/components/cms/date-input.d.ts +43 -0
- package/dist/components/cms/date-input.js +109 -0
- package/dist/components/cms/date-input.js.map +1 -0
- package/dist/components/cms/document-diff-view.js +2 -1
- package/dist/components/cms/document-diff-view.js.map +1 -1
- package/dist/components/cms/document-staging.d.ts +15 -4
- package/dist/components/cms/document-staging.js +26 -15
- package/dist/components/cms/document-staging.js.map +1 -1
- package/dist/components/cms/editable-cell.js +39 -5
- package/dist/components/cms/editable-cell.js.map +1 -1
- package/dist/components/cms/field-control.js +38 -7
- package/dist/components/cms/field-control.js.map +1 -1
- package/dist/components/cms/scalar-value-editor.js +4 -1
- package/dist/components/cms/scalar-value-editor.js.map +1 -1
- package/dist/components/cms/slug-auto-derive.d.ts +41 -0
- package/dist/components/cms/slug-auto-derive.js +14 -0
- package/dist/components/cms/slug-auto-derive.js.map +1 -0
- package/dist/components/components/transfer-dialog.js +3 -1
- package/dist/components/components/transfer-dialog.js.map +1 -1
- package/dist/components/project-session.d.ts +44 -22
- package/dist/components/project-session.js +72 -36
- package/dist/components/project-session.js.map +1 -1
- package/dist/lib/assets/asset-url.d.ts +20 -0
- package/dist/lib/assets/asset-url.js +25 -0
- package/dist/lib/assets/asset-url.js.map +1 -0
- package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
- package/dist/lib/assets/browser-blob-storage.js +86 -0
- package/dist/lib/assets/browser-blob-storage.js.map +1 -0
- package/dist/lib/assets/index.d.ts +2 -0
- package/dist/lib/assets/index.js +2 -0
- package/dist/lib/assets/index.js.map +1 -1
- package/dist/lib/data/collection-content.d.ts +20 -1
- package/dist/lib/data/collection-content.js +51 -1
- package/dist/lib/data/collection-content.js.map +1 -1
- package/dist/lib/data/content-session-refresh.d.ts +74 -0
- package/dist/lib/data/content-session-refresh.js +138 -0
- package/dist/lib/data/content-session-refresh.js.map +1 -0
- package/dist/lib/data/index.d.ts +2 -0
- package/dist/lib/data/index.js +2 -0
- package/dist/lib/data/index.js.map +1 -1
- package/dist/lib/data/playground-unload-guard.d.ts +22 -0
- package/dist/lib/data/playground-unload-guard.js +73 -0
- package/dist/lib/data/playground-unload-guard.js.map +1 -0
- package/dist/lib/i18n/messages.de.js +3 -1
- package/dist/lib/i18n/messages.de.js.map +1 -1
- package/dist/lib/i18n/messages.en.d.ts +2 -0
- package/dist/lib/i18n/messages.en.js +5 -1
- package/dist/lib/i18n/messages.en.js.map +1 -1
- package/dist/lib/persistence/index.js +6 -1
- package/dist/lib/persistence/index.js.map +1 -1
- package/dist/ui/input.d.ts +8 -3
- package/dist/ui/input.js +6 -1
- package/dist/ui/input.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content-session write-sync (EC-460) — the root-cause fix for the playground
|
|
3
|
+
* population family.
|
|
4
|
+
*
|
|
5
|
+
* The canvas preview engine (`collection-content.ts`) resolves listings,
|
|
6
|
+
* relation picks, and rich-text links against a module-global content session
|
|
7
|
+
* that used to refresh ONLY at project boot and per CMS-surface mount
|
|
8
|
+
* (`project-session.tsx`). A document authored or published after the last of
|
|
9
|
+
* those hydrations was invisible to every canvas resolver while the picker UIs
|
|
10
|
+
* (which read the live CMS store) showed it — the listing rendered its empty
|
|
11
|
+
* state against real published same-session content, and a `multiple` relation
|
|
12
|
+
* resolved 1-of-3 / 0-of-3 picked chips, all silently.
|
|
13
|
+
*
|
|
14
|
+
* This module closes the loop at the ONE write chokepoint the studio already
|
|
15
|
+
* has: the operations lifecycle event bus ("validate → authorize → mutate →
|
|
16
|
+
* emit"; its documented consumers include cache invalidation, vision AD-7 —
|
|
17
|
+
* the reference index and revalidation webhooks attach the same way). Every
|
|
18
|
+
* SUCCEEDED command in a content-bearing domain marks the session stale; a
|
|
19
|
+
* registered refresher (the project session's own `loadCms`, so both modes run
|
|
20
|
+
* the identical `loadHydratedCms` → `configureContentSession` path — no new
|
|
21
|
+
* loader, no transport bypass, no second cache) re-hydrates it, debounced so a
|
|
22
|
+
* persister burst coalesces into one load.
|
|
23
|
+
*
|
|
24
|
+
* Refreshes only run while a CONSUMER (a mounted canvas) is registered — a
|
|
25
|
+
* session with no preview surface just keeps the stale flag and re-hydrates
|
|
26
|
+
* when the next consumer arrives, so connected mode never pays a per-autosave
|
|
27
|
+
* query while nobody is previewing. The pure orchestrator
|
|
28
|
+
* ({@link createContentSessionSync}) takes its timer + refresh injected, so the
|
|
29
|
+
* coalescing/latch behaviour is unit-tested in node (house pattern); the
|
|
30
|
+
* module-level singleton below is the app wiring.
|
|
31
|
+
*/
|
|
32
|
+
/** Domains whose succeeded commands change what the preview engine serves. */
|
|
33
|
+
export const CONTENT_SESSION_WRITE_DOMAINS = new Set([
|
|
34
|
+
'documents',
|
|
35
|
+
'schema',
|
|
36
|
+
'assets',
|
|
37
|
+
]);
|
|
38
|
+
/** Whether an operation event invalidates the preview content session. */
|
|
39
|
+
export function isContentSessionWrite(event) {
|
|
40
|
+
return (event.kind === 'command' &&
|
|
41
|
+
event.status === 'succeeded' &&
|
|
42
|
+
CONTENT_SESSION_WRITE_DOMAINS.has(event.domain));
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The pure orchestrator: stale flag + consumer count + one coalesced refresh.
|
|
46
|
+
*
|
|
47
|
+
* - `markStale` with no consumers only flags; the next `addConsumer` refreshes.
|
|
48
|
+
* - `markStale` with consumers schedules a trailing debounce (bursts coalesce).
|
|
49
|
+
* - A `markStale` DURING an in-flight refresh re-runs once after it settles —
|
|
50
|
+
* the refresh that was already reading may have missed that write.
|
|
51
|
+
*/
|
|
52
|
+
export function createContentSessionSync(deps) {
|
|
53
|
+
const delayMs = deps.delayMs ?? 150;
|
|
54
|
+
const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
|
|
55
|
+
const clearTimer = deps.clearTimer ?? ((handle) => clearTimeout(handle));
|
|
56
|
+
let stale = false;
|
|
57
|
+
let consumers = 0;
|
|
58
|
+
let timer = null;
|
|
59
|
+
let running = false;
|
|
60
|
+
let rerun = false;
|
|
61
|
+
function run() {
|
|
62
|
+
if (running) {
|
|
63
|
+
rerun = true;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
running = true;
|
|
67
|
+
stale = false;
|
|
68
|
+
void deps
|
|
69
|
+
.refresh()
|
|
70
|
+
.catch(() => {
|
|
71
|
+
// A failed hydration keeps the session stale so the next trigger retries;
|
|
72
|
+
// the loader itself already reports the failure loudly (loadCmsIntoSession).
|
|
73
|
+
stale = true;
|
|
74
|
+
})
|
|
75
|
+
.finally(() => {
|
|
76
|
+
running = false;
|
|
77
|
+
if (rerun || (stale && consumers > 0)) {
|
|
78
|
+
rerun = false;
|
|
79
|
+
schedule();
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
function schedule() {
|
|
84
|
+
if (timer !== null)
|
|
85
|
+
clearTimer(timer);
|
|
86
|
+
timer = setTimer(() => {
|
|
87
|
+
timer = null;
|
|
88
|
+
run();
|
|
89
|
+
}, delayMs);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
markStale() {
|
|
93
|
+
stale = true;
|
|
94
|
+
if (consumers > 0)
|
|
95
|
+
schedule();
|
|
96
|
+
},
|
|
97
|
+
addConsumer() {
|
|
98
|
+
consumers += 1;
|
|
99
|
+
if (stale && timer === null && !running)
|
|
100
|
+
schedule();
|
|
101
|
+
let released = false;
|
|
102
|
+
return () => {
|
|
103
|
+
if (released)
|
|
104
|
+
return;
|
|
105
|
+
released = true;
|
|
106
|
+
consumers -= 1;
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
isStale() {
|
|
110
|
+
return stale;
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
let currentRefresher = null;
|
|
115
|
+
const sync = createContentSessionSync({
|
|
116
|
+
refresh: () => currentRefresher?.() ?? Promise.resolve(),
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* Register the active project's session refresher (`ProjectSessionProvider`
|
|
120
|
+
* mounts it with its `loadCms`; leaving the project unregisters). Only one is
|
|
121
|
+
* active at a time — the studio shows one project per session.
|
|
122
|
+
*/
|
|
123
|
+
export function registerContentSessionRefresher(refresher) {
|
|
124
|
+
currentRefresher = refresher;
|
|
125
|
+
return () => {
|
|
126
|
+
if (currentRefresher === refresher)
|
|
127
|
+
currentRefresher = null;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/** Mark the shared session stale (the event-bus listener calls this). */
|
|
131
|
+
export function markContentSessionStale() {
|
|
132
|
+
sync.markStale();
|
|
133
|
+
}
|
|
134
|
+
/** Register a mounted preview consumer (the canvas); returns the release. */
|
|
135
|
+
export function acquireContentSessionConsumer() {
|
|
136
|
+
return sync.addConsumer();
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=content-session-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"content-session-refresh.js","sourceRoot":"","sources":["../../../src/lib/data/content-session-refresh.ts"],"names":[],"mappings":"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","sourcesContent":["import type { OperationEvent } from '@elytracms/studio/lib/operations'\n\n/**\n * Content-session write-sync (EC-460) — the root-cause fix for the playground\n * population family.\n *\n * The canvas preview engine (`collection-content.ts`) resolves listings,\n * relation picks, and rich-text links against a module-global content session\n * that used to refresh ONLY at project boot and per CMS-surface mount\n * (`project-session.tsx`). A document authored or published after the last of\n * those hydrations was invisible to every canvas resolver while the picker UIs\n * (which read the live CMS store) showed it — the listing rendered its empty\n * state against real published same-session content, and a `multiple` relation\n * resolved 1-of-3 / 0-of-3 picked chips, all silently.\n *\n * This module closes the loop at the ONE write chokepoint the studio already\n * has: the operations lifecycle event bus (\"validate → authorize → mutate →\n * emit\"; its documented consumers include cache invalidation, vision AD-7 —\n * the reference index and revalidation webhooks attach the same way). Every\n * SUCCEEDED command in a content-bearing domain marks the session stale; a\n * registered refresher (the project session's own `loadCms`, so both modes run\n * the identical `loadHydratedCms` → `configureContentSession` path — no new\n * loader, no transport bypass, no second cache) re-hydrates it, debounced so a\n * persister burst coalesces into one load.\n *\n * Refreshes only run while a CONSUMER (a mounted canvas) is registered — a\n * session with no preview surface just keeps the stale flag and re-hydrates\n * when the next consumer arrives, so connected mode never pays a per-autosave\n * query while nobody is previewing. The pure orchestrator\n * ({@link createContentSessionSync}) takes its timer + refresh injected, so the\n * coalescing/latch behaviour is unit-tested in node (house pattern); the\n * module-level singleton below is the app wiring.\n */\n\n/** Domains whose succeeded commands change what the preview engine serves. */\nexport const CONTENT_SESSION_WRITE_DOMAINS: ReadonlySet<string> = new Set([\n 'documents',\n 'schema',\n 'assets',\n])\n\n/** Whether an operation event invalidates the preview content session. */\nexport function isContentSessionWrite(event: OperationEvent): boolean {\n return (\n event.kind === 'command' &&\n event.status === 'succeeded' &&\n CONTENT_SESSION_WRITE_DOMAINS.has(event.domain)\n )\n}\n\nexport interface ContentSessionSyncDeps {\n /** Re-hydrate the session (the project session's `loadCms`). Never throws upward. */\n refresh: () => Promise<unknown>\n /** Trailing debounce for write bursts (ms). */\n delayMs?: number\n /** Injectable timer pair (tests drive time manually). */\n setTimer?: (fn: () => void, ms: number) => unknown\n clearTimer?: (handle: unknown) => void\n}\n\nexport interface ContentSessionSync {\n /** A content-bearing write succeeded — refresh (debounced) if anyone previews. */\n markStale(): void\n /** A preview surface mounted; returns its release. A stale session refreshes now. */\n addConsumer(): () => void\n /** Whether a refresh is currently owed (test/introspection seam). */\n isStale(): boolean\n}\n\n/**\n * The pure orchestrator: stale flag + consumer count + one coalesced refresh.\n *\n * - `markStale` with no consumers only flags; the next `addConsumer` refreshes.\n * - `markStale` with consumers schedules a trailing debounce (bursts coalesce).\n * - A `markStale` DURING an in-flight refresh re-runs once after it settles —\n * the refresh that was already reading may have missed that write.\n */\nexport function createContentSessionSync(deps: ContentSessionSyncDeps): ContentSessionSync {\n const delayMs = deps.delayMs ?? 150\n const setTimer = deps.setTimer ?? ((fn: () => void, ms: number) => setTimeout(fn, ms))\n const clearTimer = deps.clearTimer ?? ((handle: unknown) => clearTimeout(handle as number))\n\n let stale = false\n let consumers = 0\n let timer: unknown = null\n let running = false\n let rerun = false\n\n function run(): void {\n if (running) {\n rerun = true\n return\n }\n running = true\n stale = false\n void deps\n .refresh()\n .catch(() => {\n // A failed hydration keeps the session stale so the next trigger retries;\n // the loader itself already reports the failure loudly (loadCmsIntoSession).\n stale = true\n })\n .finally(() => {\n running = false\n if (rerun || (stale && consumers > 0)) {\n rerun = false\n schedule()\n }\n })\n }\n\n function schedule(): void {\n if (timer !== null) clearTimer(timer)\n timer = setTimer(() => {\n timer = null\n run()\n }, delayMs)\n }\n\n return {\n markStale() {\n stale = true\n if (consumers > 0) schedule()\n },\n addConsumer() {\n consumers += 1\n if (stale && timer === null && !running) schedule()\n let released = false\n return () => {\n if (released) return\n released = true\n consumers -= 1\n }\n },\n isStale() {\n return stale\n },\n }\n}\n\n// --- App wiring (module singleton) -------------------------------------------------\n\ntype Refresher = () => Promise<unknown>\n\nlet currentRefresher: Refresher | null = null\n\nconst sync = createContentSessionSync({\n refresh: () => currentRefresher?.() ?? Promise.resolve(),\n})\n\n/**\n * Register the active project's session refresher (`ProjectSessionProvider`\n * mounts it with its `loadCms`; leaving the project unregisters). Only one is\n * active at a time — the studio shows one project per session.\n */\nexport function registerContentSessionRefresher(refresher: Refresher): () => void {\n currentRefresher = refresher\n return () => {\n if (currentRefresher === refresher) currentRefresher = null\n }\n}\n\n/** Mark the shared session stale (the event-bus listener calls this). */\nexport function markContentSessionStale(): void {\n sync.markStale()\n}\n\n/** Register a mounted preview consumer (the canvas); returns the release. */\nexport function acquireContentSessionConsumer(): () => void {\n return sync.addConsumer()\n}\n"]}
|
package/dist/lib/data/index.d.ts
CHANGED
package/dist/lib/data/index.js
CHANGED
|
@@ -2,6 +2,8 @@ export * from './data-store.js';
|
|
|
2
2
|
export * from './sample-capture.js';
|
|
3
3
|
export * from './sources-store.js';
|
|
4
4
|
export * from './collection-content.js';
|
|
5
|
+
export * from './content-session-refresh.js';
|
|
6
|
+
export * from './playground-unload-guard.js';
|
|
5
7
|
// Data providers (EC-167) are imported from './providers' directly: the module
|
|
6
8
|
// exports a `DataProvider` domain type that would collide with the store's
|
|
7
9
|
// `<DataProvider>` React provider in a wildcard barrel.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD","sourcesContent":["export * from './data-store'\nexport * from './sample-capture'\nexport * from './sources-store'\nexport * from './collection-content'\n// Data providers (EC-167) are imported from './providers' directly: the module\n// exports a `DataProvider` domain type that would collide with the store's\n// `<DataProvider>` React provider in a wildcard barrel.\n"]}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/lib/data/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAA;AAC5B,cAAc,kBAAkB,CAAA;AAChC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,sBAAsB,CAAA;AACpC,cAAc,2BAA2B,CAAA;AACzC,cAAc,2BAA2B,CAAA;AACzC,+EAA+E;AAC/E,2EAA2E;AAC3E,wDAAwD","sourcesContent":["export * from './data-store'\nexport * from './sample-capture'\nexport * from './sources-store'\nexport * from './collection-content'\nexport * from './content-session-refresh'\nexport * from './playground-unload-guard'\n// Data providers (EC-167) are imported from './providers' directly: the module\n// exports a `DataProvider` domain type that would collide with the store's\n// `<DataProvider>` React provider in a wildcard barrel.\n"]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { OperationEvent } from '@elytracms/studio/lib/operations';
|
|
2
|
+
/** Should this operation event arm the playground unload guard? */
|
|
3
|
+
export declare function shouldArmUnloadGuard(event: OperationEvent, playground?: boolean): boolean;
|
|
4
|
+
export interface UnloadGuardState {
|
|
5
|
+
/** A qualifying write happened — register the listener once. */
|
|
6
|
+
arm(): void;
|
|
7
|
+
/** Is the guard live (listener registered)? */
|
|
8
|
+
isArmed(): boolean;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The arm-once state machine. `register` is the injected DOM seam — it is
|
|
12
|
+
* called exactly once, on the first arm, with the handler that flags the
|
|
13
|
+
* unload prompt (`event.preventDefault()` + legacy `returnValue`).
|
|
14
|
+
*/
|
|
15
|
+
export declare function createUnloadGuardState(register: (handler: (event: BeforeUnloadEvent) => void) => void): UnloadGuardState;
|
|
16
|
+
/**
|
|
17
|
+
* Feed an operation event to the guard (the project session's event-bus
|
|
18
|
+
* subscription calls this alongside the content-session staleness mark).
|
|
19
|
+
*/
|
|
20
|
+
export declare function notePlaygroundWriteForUnloadGuard(event: OperationEvent): void;
|
|
21
|
+
/** Introspection seam (live verification + tests): is the page-level guard armed? */
|
|
22
|
+
export declare function isPlaygroundUnloadGuardArmed(): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EC-461 — the playground `beforeunload` guard.
|
|
3
|
+
*
|
|
4
|
+
* The playground's world is one in-memory adapter: a full page unload — a
|
|
5
|
+
* reload, a Vite full-reload after a source edit (HMR), a hard top-level
|
|
6
|
+
* navigation, a closed tab — destroys every authored document, schema change,
|
|
7
|
+
* asset and publish irrecoverably. That is documented semantics, but losing it
|
|
8
|
+
* SILENTLY is not acceptable once the session actually holds authored state.
|
|
9
|
+
*
|
|
10
|
+
* The rule (stated once, applied everywhere): the guard arms in PLAYGROUND
|
|
11
|
+
* mode only, the first time a content-bearing write command succeeds on the
|
|
12
|
+
* operations event bus (the same `isContentSessionWrite` predicate the EC-460
|
|
13
|
+
* content-session sync uses — documents/schema/assets), and never disarms for
|
|
14
|
+
* the rest of the page's life: any such write means the world has diverged
|
|
15
|
+
* from its deterministic seed, and nothing short of the unload itself brings
|
|
16
|
+
* it back. Connected mode never arms — writes persist to the backend within
|
|
17
|
+
* the autosave rhythm, in-app navigation flushes pending autosaves on unmount,
|
|
18
|
+
* and explicit-save rails carry their own `UnsavedChangesGuard`; a blanket
|
|
19
|
+
* unload prompt there would cry wolf on every visit.
|
|
20
|
+
*
|
|
21
|
+
* The decision core ({@link createUnloadGuardState}) is pure (house pattern);
|
|
22
|
+
* the module singleton below injects the real `window` listener registration.
|
|
23
|
+
* Browsers require sticky user activation to actually show the dialog — an
|
|
24
|
+
* un-interacted-with tab reloads silently regardless, which is fine: any
|
|
25
|
+
* authored write implies interaction.
|
|
26
|
+
*/
|
|
27
|
+
import { isPlaygroundMode } from '@elytracms/studio/lib/persistence';
|
|
28
|
+
import { isContentSessionWrite } from './content-session-refresh.js';
|
|
29
|
+
/** Should this operation event arm the playground unload guard? */
|
|
30
|
+
export function shouldArmUnloadGuard(event, playground = isPlaygroundMode()) {
|
|
31
|
+
return playground && isContentSessionWrite(event);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The arm-once state machine. `register` is the injected DOM seam — it is
|
|
35
|
+
* called exactly once, on the first arm, with the handler that flags the
|
|
36
|
+
* unload prompt (`event.preventDefault()` + legacy `returnValue`).
|
|
37
|
+
*/
|
|
38
|
+
export function createUnloadGuardState(register) {
|
|
39
|
+
let armed = false;
|
|
40
|
+
return {
|
|
41
|
+
arm() {
|
|
42
|
+
if (armed)
|
|
43
|
+
return;
|
|
44
|
+
armed = true;
|
|
45
|
+
register((event) => {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
// Legacy channel — some browsers still require a truthy returnValue.
|
|
48
|
+
event.returnValue = '';
|
|
49
|
+
});
|
|
50
|
+
},
|
|
51
|
+
isArmed() {
|
|
52
|
+
return armed;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// --- App wiring (module singleton) -------------------------------------------------
|
|
57
|
+
const guard = createUnloadGuardState((handler) => {
|
|
58
|
+
if (typeof window !== 'undefined')
|
|
59
|
+
window.addEventListener('beforeunload', handler);
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Feed an operation event to the guard (the project session's event-bus
|
|
63
|
+
* subscription calls this alongside the content-session staleness mark).
|
|
64
|
+
*/
|
|
65
|
+
export function notePlaygroundWriteForUnloadGuard(event) {
|
|
66
|
+
if (shouldArmUnloadGuard(event))
|
|
67
|
+
guard.arm();
|
|
68
|
+
}
|
|
69
|
+
/** Introspection seam (live verification + tests): is the page-level guard armed? */
|
|
70
|
+
export function isPlaygroundUnloadGuardArmed() {
|
|
71
|
+
return guard.isArmed();
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=playground-unload-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"playground-unload-guard.js","sourceRoot":"","sources":["../../../src/lib/data/playground-unload-guard.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AAEpE,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAA;AAEjE,mEAAmE;AACnE,MAAM,UAAU,oBAAoB,CAClC,KAAqB,EACrB,aAAsB,gBAAgB,EAAE;IAExC,OAAO,UAAU,IAAI,qBAAqB,CAAC,KAAK,CAAC,CAAA;AACnD,CAAC;AASD;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CACpC,QAA+D;IAE/D,IAAI,KAAK,GAAG,KAAK,CAAA;IACjB,OAAO;QACL,GAAG;YACD,IAAI,KAAK;gBAAE,OAAM;YACjB,KAAK,GAAG,IAAI,CAAA;YACZ,QAAQ,CAAC,CAAC,KAAK,EAAE,EAAE;gBACjB,KAAK,CAAC,cAAc,EAAE,CAAA;gBACtB,qEAAqE;gBACrE,KAAK,CAAC,WAAW,GAAG,EAAE,CAAA;YACxB,CAAC,CAAC,CAAA;QACJ,CAAC;QACD,OAAO;YACL,OAAO,KAAK,CAAA;QACd,CAAC;KACF,CAAA;AACH,CAAC;AAED,sFAAsF;AAEtF,MAAM,KAAK,GAAG,sBAAsB,CAAC,CAAC,OAAO,EAAE,EAAE;IAC/C,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,CAAA;AACrF,CAAC,CAAC,CAAA;AAEF;;;GAGG;AACH,MAAM,UAAU,iCAAiC,CAAC,KAAqB;IACrE,IAAI,oBAAoB,CAAC,KAAK,CAAC;QAAE,KAAK,CAAC,GAAG,EAAE,CAAA;AAC9C,CAAC;AAED,qFAAqF;AACrF,MAAM,UAAU,4BAA4B;IAC1C,OAAO,KAAK,CAAC,OAAO,EAAE,CAAA;AACxB,CAAC","sourcesContent":["/**\n * EC-461 — the playground `beforeunload` guard.\n *\n * The playground's world is one in-memory adapter: a full page unload — a\n * reload, a Vite full-reload after a source edit (HMR), a hard top-level\n * navigation, a closed tab — destroys every authored document, schema change,\n * asset and publish irrecoverably. That is documented semantics, but losing it\n * SILENTLY is not acceptable once the session actually holds authored state.\n *\n * The rule (stated once, applied everywhere): the guard arms in PLAYGROUND\n * mode only, the first time a content-bearing write command succeeds on the\n * operations event bus (the same `isContentSessionWrite` predicate the EC-460\n * content-session sync uses — documents/schema/assets), and never disarms for\n * the rest of the page's life: any such write means the world has diverged\n * from its deterministic seed, and nothing short of the unload itself brings\n * it back. Connected mode never arms — writes persist to the backend within\n * the autosave rhythm, in-app navigation flushes pending autosaves on unmount,\n * and explicit-save rails carry their own `UnsavedChangesGuard`; a blanket\n * unload prompt there would cry wolf on every visit.\n *\n * The decision core ({@link createUnloadGuardState}) is pure (house pattern);\n * the module singleton below injects the real `window` listener registration.\n * Browsers require sticky user activation to actually show the dialog — an\n * un-interacted-with tab reloads silently regardless, which is fine: any\n * authored write implies interaction.\n */\nimport { isPlaygroundMode } from '@elytracms/studio/lib/persistence'\nimport type { OperationEvent } from '@elytracms/studio/lib/operations'\nimport { isContentSessionWrite } from './content-session-refresh'\n\n/** Should this operation event arm the playground unload guard? */\nexport function shouldArmUnloadGuard(\n event: OperationEvent,\n playground: boolean = isPlaygroundMode(),\n): boolean {\n return playground && isContentSessionWrite(event)\n}\n\nexport interface UnloadGuardState {\n /** A qualifying write happened — register the listener once. */\n arm(): void\n /** Is the guard live (listener registered)? */\n isArmed(): boolean\n}\n\n/**\n * The arm-once state machine. `register` is the injected DOM seam — it is\n * called exactly once, on the first arm, with the handler that flags the\n * unload prompt (`event.preventDefault()` + legacy `returnValue`).\n */\nexport function createUnloadGuardState(\n register: (handler: (event: BeforeUnloadEvent) => void) => void,\n): UnloadGuardState {\n let armed = false\n return {\n arm() {\n if (armed) return\n armed = true\n register((event) => {\n event.preventDefault()\n // Legacy channel — some browsers still require a truthy returnValue.\n event.returnValue = ''\n })\n },\n isArmed() {\n return armed\n },\n }\n}\n\n// --- App wiring (module singleton) -------------------------------------------------\n\nconst guard = createUnloadGuardState((handler) => {\n if (typeof window !== 'undefined') window.addEventListener('beforeunload', handler)\n})\n\n/**\n * Feed an operation event to the guard (the project session's event-bus\n * subscription calls this alongside the content-session staleness mark).\n */\nexport function notePlaygroundWriteForUnloadGuard(event: OperationEvent): void {\n if (shouldArmUnloadGuard(event)) guard.arm()\n}\n\n/** Introspection seam (live verification + tests): is the page-level guard armed? */\nexport function isPlaygroundUnloadGuardArmed(): boolean {\n return guard.isArmed()\n}\n"]}
|
|
@@ -215,7 +215,7 @@ export const deMessages = {
|
|
|
215
215
|
'cms.list.emptyNone': 'Noch keine {noun}.',
|
|
216
216
|
'cms.list.emptyFiltered': 'Keine Dokumente entsprechen den Filtern der aktuellen Ansicht.',
|
|
217
217
|
'cms.list.selectAll': 'Alle Dokumente in der Ansicht auswählen',
|
|
218
|
-
'cms.list.selectRow': '{
|
|
218
|
+
'cms.list.selectRow': '{title} auswählen',
|
|
219
219
|
'cms.list.column.title': 'Titel',
|
|
220
220
|
'cms.list.column.state': 'Status',
|
|
221
221
|
// EC-448 — Icon-Header der fixierten Status-Punkt-Spalte (nur `aria-label`/`title`).
|
|
@@ -306,6 +306,7 @@ export const deMessages = {
|
|
|
306
306
|
'cms.cell.blockCount.other': '{count} Blöcke',
|
|
307
307
|
'cms.cell.createsVariantTitle': 'Beim Speichern wird die {locale}-Übersetzung dieses Feldes erstellt.',
|
|
308
308
|
'cms.cell.newVariant': 'neu',
|
|
309
|
+
'cms.cell.editAria': '{field} von {title} bearbeiten',
|
|
309
310
|
// ─────────────────────────────────────────────────────────────────────────
|
|
310
311
|
// CMS — document detail editor
|
|
311
312
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -940,6 +941,7 @@ export const deMessages = {
|
|
|
940
941
|
'builder.canvas.reusableMissingTitle': 'Dieses wiederverwendbare Element fehlt',
|
|
941
942
|
'builder.canvas.reusableUnpublishedHint': 'Veröffentlichen Sie es, um es hier und auf Live-Seiten anzuzeigen — oder öffnen Sie es im Eigenschaften-Panel, um weiter zu bearbeiten.',
|
|
942
943
|
'builder.canvas.reusableMissingHint': 'Öffnen Sie das „Element“ der Einbettung im Eigenschaften-Panel, um die Referenz zu korrigieren.',
|
|
944
|
+
'builder.canvas.listingInvalidTitle': 'Diese Liste kann nicht ausgeführt werden:',
|
|
943
945
|
'builder.canvas.viewportPreviewAria': '{label} Vorschau',
|
|
944
946
|
'builder.canvas.viewportWidthTitle': '{label} Breite',
|
|
945
947
|
'builder.canvas.breakpointsIssueLabel': 'Breakpoints',
|