@elytracms/studio 0.0.78 → 0.0.80

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 (85) hide show
  1. package/dist/components/app-views/default-modules.js +5 -1
  2. package/dist/components/app-views/default-modules.js.map +1 -1
  3. package/dist/components/assets/asset-thumb.js +12 -11
  4. package/dist/components/assets/asset-thumb.js.map +1 -1
  5. package/dist/components/builder/blocks-panel.d.ts +11 -5
  6. package/dist/components/builder/blocks-panel.js +18 -24
  7. package/dist/components/builder/blocks-panel.js.map +1 -1
  8. package/dist/components/builder/canvas-render-context.d.ts +22 -0
  9. package/dist/components/builder/canvas-render-context.js +65 -9
  10. package/dist/components/builder/canvas-render-context.js.map +1 -1
  11. package/dist/components/builder/reuse.d.ts +25 -0
  12. package/dist/components/builder/reuse.js +11 -2
  13. package/dist/components/builder/reuse.js.map +1 -1
  14. package/dist/components/cms/asset-field.js +52 -19
  15. package/dist/components/cms/asset-field.js.map +1 -1
  16. package/dist/components/cms/asset-picker-core.d.ts +40 -0
  17. package/dist/components/cms/asset-picker-core.js +21 -0
  18. package/dist/components/cms/asset-picker-core.js.map +1 -1
  19. package/dist/components/cms/asset-picker.d.ts +11 -2
  20. package/dist/components/cms/asset-picker.js +34 -13
  21. package/dist/components/cms/asset-picker.js.map +1 -1
  22. package/dist/components/cms/cell.js +2 -1
  23. package/dist/components/cms/cell.js.map +1 -1
  24. package/dist/components/cms/cms-store.d.ts +8 -1
  25. package/dist/components/cms/cms-store.js +8 -7
  26. package/dist/components/cms/cms-store.js.map +1 -1
  27. package/dist/components/cms/collection-table.js +7 -1
  28. package/dist/components/cms/collection-table.js.map +1 -1
  29. package/dist/components/cms/date-input-core.d.ts +85 -0
  30. package/dist/components/cms/date-input-core.js +98 -0
  31. package/dist/components/cms/date-input-core.js.map +1 -0
  32. package/dist/components/cms/date-input.d.ts +51 -0
  33. package/dist/components/cms/date-input.js +109 -0
  34. package/dist/components/cms/date-input.js.map +1 -0
  35. package/dist/components/cms/document-diff-view.js +2 -1
  36. package/dist/components/cms/document-diff-view.js.map +1 -1
  37. package/dist/components/cms/document-staging.d.ts +15 -4
  38. package/dist/components/cms/document-staging.js +26 -15
  39. package/dist/components/cms/document-staging.js.map +1 -1
  40. package/dist/components/cms/editable-cell.js +40 -5
  41. package/dist/components/cms/editable-cell.js.map +1 -1
  42. package/dist/components/cms/field-control.js +39 -7
  43. package/dist/components/cms/field-control.js.map +1 -1
  44. package/dist/components/cms/scalar-value-editor.js +5 -1
  45. package/dist/components/cms/scalar-value-editor.js.map +1 -1
  46. package/dist/components/cms/slug-auto-derive.d.ts +41 -0
  47. package/dist/components/cms/slug-auto-derive.js +14 -0
  48. package/dist/components/cms/slug-auto-derive.js.map +1 -0
  49. package/dist/components/components/transfer-dialog.js +3 -1
  50. package/dist/components/components/transfer-dialog.js.map +1 -1
  51. package/dist/components/project-session.d.ts +44 -22
  52. package/dist/components/project-session.js +72 -36
  53. package/dist/components/project-session.js.map +1 -1
  54. package/dist/lib/assets/asset-url.d.ts +20 -0
  55. package/dist/lib/assets/asset-url.js +25 -0
  56. package/dist/lib/assets/asset-url.js.map +1 -0
  57. package/dist/lib/assets/browser-blob-storage.d.ts +80 -0
  58. package/dist/lib/assets/browser-blob-storage.js +86 -0
  59. package/dist/lib/assets/browser-blob-storage.js.map +1 -0
  60. package/dist/lib/assets/index.d.ts +2 -0
  61. package/dist/lib/assets/index.js +2 -0
  62. package/dist/lib/assets/index.js.map +1 -1
  63. package/dist/lib/data/collection-content.d.ts +20 -1
  64. package/dist/lib/data/collection-content.js +51 -1
  65. package/dist/lib/data/collection-content.js.map +1 -1
  66. package/dist/lib/data/content-session-refresh.d.ts +74 -0
  67. package/dist/lib/data/content-session-refresh.js +138 -0
  68. package/dist/lib/data/content-session-refresh.js.map +1 -0
  69. package/dist/lib/data/index.d.ts +2 -0
  70. package/dist/lib/data/index.js +2 -0
  71. package/dist/lib/data/index.js.map +1 -1
  72. package/dist/lib/data/playground-unload-guard.d.ts +22 -0
  73. package/dist/lib/data/playground-unload-guard.js +73 -0
  74. package/dist/lib/data/playground-unload-guard.js.map +1 -0
  75. package/dist/lib/i18n/messages.de.js +3 -1
  76. package/dist/lib/i18n/messages.de.js.map +1 -1
  77. package/dist/lib/i18n/messages.en.d.ts +2 -0
  78. package/dist/lib/i18n/messages.en.js +5 -1
  79. package/dist/lib/i18n/messages.en.js.map +1 -1
  80. package/dist/lib/persistence/index.js +6 -1
  81. package/dist/lib/persistence/index.js.map +1 -1
  82. package/dist/ui/input.d.ts +8 -3
  83. package/dist/ui/input.js +6 -1
  84. package/dist/ui/input.js.map +1 -1
  85. package/package.json +2 -2
@@ -1 +1 @@
1
- {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
1
+ {"version":3,"file":"collection-content.js","sourceRoot":"","sources":["../../../src/lib/data/collection-content.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n createContentClient,\n createContentContext,\n queryError,\n validateLimit,\n validateSort,\n validateWhere,\n} from '@elytracms/core/content'\nimport type {\n ContentClient,\n ContentDocumentSource,\n ContentLookup,\n ContentQueryError,\n GetDocumentResult,\n ListDocumentsQuery,\n ListDocumentsResult,\n ResolvedDocument,\n} from '@elytracms/core/content'\nimport { documentKey, resolveDocumentTitle, sampleLocaleConfig } from '@elytracms/core/cms-core'\nimport type {\n CmsDocument,\n CollectionDef,\n DocumentRef,\n Locale,\n LocaleConfig,\n RouteRecord,\n} from '@elytracms/core/cms-core'\nimport type { AssetRecord } from '@elytracms/core/persistence'\nimport type { ListingQuery } from '@elytracms/core/runtime-renderer'\nimport { getStudioLocale } from '../locale/locale-store'\n\n/**\n * The studio's content delivery engine for canvas previews (EC-166).\n *\n * Repeating sections and template pages preview *delivery-shaped* documents —\n * the exact shape `@elytracms/core/content` serves in production — so the canvas\n * shows what publishing will show. This module wires the canonical content\n * client (closed AD-3 query vocabulary, published perspective, EC-018 locale\n * fallback, depth-1 population, resolved assets) over a **content session**:\n *\n * - Convex mode: the project-session gate (`components/studio/project-session`)\n * configures the session with the project's HYDRATED schema + documents, so\n * \"Preview with\" lists and section previews show the live content — never\n * fixture titles (EC-177 gap 1).\n * - Playground mode (EC-361): the SAME hydration loader runs over the seeded\n * in-memory adapter (through operations — AD-17), so canvas listing/relation\n * population resolves from the in-memory session documents with identical\n * semantics. With no session configured (tests, boot instants, no persisted\n * content) the engine serves nothing.\n *\n * Nothing here invents query semantics; both modes go through the same\n * canonical client.\n */\n\n/** The schema + documents the preview engine serves (one project session). */\nexport interface ContentSessionState {\n collections: readonly CollectionDef[]\n documents: readonly CmsDocument[]\n /**\n * Published document keys via the version POINTER (EC-224). When present, the\n * preview's published perspective resolves by the pin — never the flat\n * `state` enum — so it matches what delivery serves. Absent (playground\n * fixtures / tests) → the rows' own `state` gates the perspective, unchanged.\n */\n publishedKeys?: readonly string[]\n /** Locale config of the session content; defaults to the sample config. */\n localeConfig?: LocaleConfig\n /**\n * The project's config-sourced routes (EC-207). Wired into the content client so\n * the canvas preview's `resolveLink` (EC-291) derives a rich-text link's URL from\n * the SAME routes delivery serves — the studio-live half of the host link bake.\n * Absent (playground/tests) → no routes, so `resolveLink` returns null and a\n * reference link degrades to the visible `unresolved-ref` issue.\n */\n routes?: readonly RouteRecord[]\n /**\n * The project's asset records (assets table). Wired into the lookup so the\n * content client resolves `asset`-typed FIELDS on listing/relation results\n * (e.g. a ProductGrid product's `gallery`) to delivery shape — the same assets\n * the renderer's `resolveAsset` reads for static props (EC-262). Absent\n * (playground/tests) → assets come only from `asset`-collection fixture docs.\n */\n assets?: readonly AssetRecord[]\n}\n\n/**\n * Default (no-session) collection schema: EMPTY. The studio ships no demo content\n * (Oli, 2026-06-19) — with no backend/content session configured, there is nothing\n * to preview, and the studio says so rather than serving fixtures. A real project's\n * collections + documents arrive via {@link configureContentSession} in BOTH modes\n * (EC-361): Convex hydration, or the playground's seeded in-memory adapter through\n * the same loader.\n */\nexport const playgroundCollections: readonly CollectionDef[] = []\n\n/**\n * The perspective a canvas preview resolves references against (EC-300/AD-16). The\n * canvas is draft-coherent: the editable Draft view resolves references to their\n * `'draft'` targets (so a just-made reusable shows immediately), the read-only\n * Published view resolves `'published'` (an honest preview of what delivery serves;\n * an unpublished hard ref shows the in-canvas placeholder). Delivery stays\n * published-only — this does not weaken `live-is-published-only`.\n */\nexport type CanvasPerspective = 'draft' | 'published'\n\nlet contentSession: ContentSessionState | null = null\nlet lookup: ContentLookup = createLookup(playgroundCollections, [], [])\n// Keyed by `${locale}:${perspective}` (EC-300) — one deterministic client per pair.\nconst clients = new Map<string, ContentClient>()\n\n// EC-460: a monotonically increasing session epoch + listener set, so a REACT\n// surface (the canvas render context) can re-render when the session is\n// re-pointed at fresher content. The session itself stays a module global (the\n// EC-361 design); this only makes \"it changed\" observable — `configureContentSession`\n// remains the single setter.\nlet sessionEpoch = 0\nconst sessionListeners = new Set<() => void>()\n\n/** The current content-session epoch — bumps on every `configureContentSession`. */\nexport function contentSessionEpoch(): number {\n return sessionEpoch\n}\n\n/**\n * Subscribe to content-session swaps (EC-460). The listener fires after every\n * `configureContentSession`; returns the unsubscribe. Shaped for\n * `useSyncExternalStore(subscribeContentSession, contentSessionEpoch)`.\n */\nexport function subscribeContentSession(listener: () => void): () => void {\n sessionListeners.add(listener)\n return () => sessionListeners.delete(listener)\n}\n\n/**\n * Point the preview engine at a project's hydrated content — Convex hydration\n * or the playground's in-memory persisted state (EC-361), both through the same\n * `loadHydratedCms` loader — or back at the empty no-session default (`null`).\n */\nexport function configureContentSession(next: ContentSessionState | null): void {\n contentSession = next\n lookup = next\n ? createLookup(next.collections, next.documents, next.publishedKeys, next.assets)\n : createLookup(playgroundCollections, [], [])\n clients.clear()\n sessionEpoch += 1\n for (const listener of [...sessionListeners]) listener()\n}\n\n/** Whether a hydrated content session is configured (Convex mode). */\nexport function hasContentSession(): boolean {\n return contentSession !== null\n}\n\n/** The collections the preview engine currently serves (session or fixtures). */\nfunction activeCollections(): readonly CollectionDef[] {\n return contentSession?.collections ?? playgroundCollections\n}\n\nfunction activeLocaleConfig(): LocaleConfig {\n return contentSession?.localeConfig ?? sampleLocaleConfig\n}\n\n/** Resolve a collection in the active content session (fixtures in playground). */\nexport function playgroundCollection(id: string): CollectionDef | undefined {\n return activeCollections().find((collection) => collection.id === id)\n}\n\n/**\n * Overlay the active session's canonical `filterable` declarations onto a\n * schema that doesn't carry any of its own. The inspector hands the Content\n * tab fixture collection defs; the filter/sort pickers must offer exactly the\n * EC-143 declaration set the delivery engine enforces, so both ends agree. A\n * collection that declares its own filterable fields is left untouched.\n */\nexport function withPlaygroundFilterable(\n collections: readonly CollectionDef[],\n): readonly CollectionDef[] {\n return collections.map((collection) => {\n if (collection.fields.some((field) => field.filterable === true)) return collection\n const counterpart = playgroundCollection(collection.id)\n if (counterpart === undefined) return collection\n const flagged = new Set(\n counterpart.fields.filter((field) => field.filterable === true).map((field) => field.name),\n )\n if (flagged.size === 0) return collection\n return {\n ...collection,\n fields: collection.fields.map((field) =>\n flagged.has(field.name) ? { ...field, filterable: true } : field,\n ),\n }\n })\n}\n\n/** Synthesize the persistence-shaped asset record the asset resolver reads. */\nfunction assetRecordOf(doc: CmsDocument): AssetRecord {\n const values = doc.values\n const alt = doc.localized?.en?.alt\n return {\n id: doc.id,\n projectId: 'playground',\n filename: typeof values.filename === 'string' ? values.filename : doc.id,\n contentType: 'image/png',\n byteSize: 0,\n alt: typeof alt === 'string' ? alt : undefined,\n storageKey: typeof values.url === 'string' ? values.url : doc.id,\n createdAt: '2026-01-01T00:00:00.000Z',\n updatedAt: '2026-01-01T00:00:00.000Z',\n }\n}\n\nfunction createLookup(\n collectionDefs: readonly CollectionDef[],\n documents: readonly CmsDocument[],\n publishedKeys?: readonly string[],\n assetRecords?: readonly AssetRecord[],\n): ContentLookup {\n const collections = new Map(collectionDefs.map((c) => [c.id, c]))\n // EC-224: published-ness is the version POINTER, not the flat `state` enum.\n // A hydrated session hands `publishedKeys` (the pinned set) → wrap each row as\n // a `DocumentHistory` so the published perspective resolves by the pin (the\n // live row is always a draft now) with ZERO dependence on `state`. Without a\n // pointer set (playground fixtures / tests) we keep bare rows, whose own\n // `state` gates the published perspective — byte-identical to before.\n const published = publishedKeys ? new Set(publishedKeys) : null\n const toSource = (doc: CmsDocument): ContentDocumentSource => {\n if (!published) return doc\n return published.has(documentKey({ collection: doc.collection, id: doc.id }))\n ? { draft: doc, published: doc, versions: [] }\n : { draft: doc, published: undefined, versions: [] }\n }\n const byKey = new Map(\n documents.map((doc) => [`${doc.collection}:${doc.id}`, toSource(doc)] as const),\n )\n // Asset resolution source for `asset`-typed FIELDS on resolved documents\n // (listing/relation results, e.g. a product's `gallery`). Two sources, real\n // records last so they win: (1) `asset`-collection fixture documents — how\n // playground/tests model assets; (2) the project's real asset-table records\n // (EC-262) — a Convex session has its assets here, NOT as `asset` documents,\n // so without these the content client resolves no nested image.\n const assets = new Map<string, AssetRecord>()\n for (const doc of documents) {\n if (doc.collection === 'asset') assets.set(doc.id, assetRecordOf(doc))\n }\n for (const record of assetRecords ?? []) assets.set(record.id, record)\n return {\n collection: (id) => collections.get(id),\n document: (ref) => byKey.get(`${ref.collection}:${ref.id}`),\n documents: (collectionId) =>\n documents.filter((doc) => doc.collection === collectionId).map(toSource),\n asset: (id) => assets.get(id),\n // Prefer the record's resolved servable `url` — the one the renderer's\n // resolveAsset reads (EC-262). Real asset-table records carry a Convex blob\n // locator in `storageKey` and the servable URL in `url`; fixture/`asset`-document\n // records (assetRecordOf) leave `url` unset and store the servable url in\n // `storageKey`. Falling back keeps both correct.\n assetUrl: (asset) => asset.url ?? asset.storageKey,\n }\n}\n\n/**\n * One deterministic client per (locale, perspective): EC-018 locale fallback,\n * draft-or-published perspective (EC-300/AD-16, default `'published'`). The studio's\n * ambient active locale (EC-176, `lib/locale`) picks the locale at query time, so the\n * canvas previews follow the locale switcher exactly like delivery would; the canvas's\n * Draft/Published view picks the perspective. The cache resets whenever the content\n * session changes.\n */\nfunction clientFor(locale: Locale, perspective: CanvasPerspective = 'published'): ContentClient {\n const key = `${locale}:${perspective}`\n const existing = clients.get(key)\n if (existing) return existing\n const client = createContentClient({\n lookup,\n context: createContentContext({\n locale,\n perspective,\n locales: activeLocaleConfig(),\n }),\n // EC-291: the routes a rich-text link resolves against (config-sourced) — the\n // studio-live half of the host link bake.\n routes: contentSession?.routes ?? [],\n })\n clients.set(key, client)\n return client\n}\n\n/**\n * Run a closed-vocabulary list query (AD-3: eq/in/lt/lte/gt/gte, AND-only,\n * filterable fields only) and return the delivery-shaped result. Invalid\n * queries come back as structured errors — never thrown, never silently\n * widened. Resolves in the studio's active locale unless one is passed.\n */\nexport function runCollectionListQuery(\n collectionId: string,\n query: ListDocumentsQuery = {},\n locale: Locale = getStudioLocale(),\n): ListDocumentsResult {\n return clientFor(locale).listDocuments(collectionId, query)\n}\n\n/** Resolve one document (by id or slug) into delivery shape. */\nexport function getCollectionDocument(\n collectionId: string,\n idOrSlug: string,\n locale: Locale = getStudioLocale(),\n): GetDocumentResult {\n return clientFor(locale).getDocument(collectionId, idOrSlug)\n}\n\n/**\n * Populate a composition-node `relation` prop to delivery shape for the canvas\n * (EC-254) — the studio analog of the host's baked relations map. Resolves the\n * ref through the active session's client (published perspective, active locale),\n * so the canvas previews a block's picked documents (a ProductGrid's products, a\n * Reusable's element) exactly as delivery renders them. `null` when the target is\n * missing or not visible → the block degrades to its own empty state.\n */\nexport function resolveCanvasRelation(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument | null {\n return clientFor(locale, perspective).resolveRelation(ref)\n}\n\n/**\n * Resolve a listing block's query to its documents for the canvas (EC-255) — the\n * studio analog of the host's baked listings map. Runs the query (collection +\n * AND-only filter, optional sort/limit) through the active session's client\n * (published perspective, active locale), so the canvas previews a dynamic grid (a\n * ProductGrid keyed to a category) exactly as delivery renders it. An invalid query\n * (structured, never thrown) or a non-list lookup yields `[]` → the block's empty\n * state. Reuses the same `listDocuments` path as `runCollectionListQuery`.\n */\nexport function resolveCanvasListing(\n query: ListingQuery,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): ResolvedDocument[] {\n const result = clientFor(locale, perspective).listDocuments(query.collection, {\n where: query.where,\n ...(query.sort ? { sort: query.sort } : {}),\n ...(query.limit !== undefined ? { limit: query.limit } : {}),\n })\n return result.ok ? result.documents : []\n}\n\n/**\n * Statically validate a listing block's query against the ACTIVE session schema\n * (EC-460, the never-silent rule): the exact `validateWhere`/`validateSort`/\n * `validateLimit` checks `listDocuments` runs, WITHOUT executing the query — so\n * the canvas can surface a config-invalid listing (a filter/sort field the\n * collection doesn't declare `filterable`, an unknown collection) as a visible\n * notice instead of the block's indistinguishable empty state. With no content\n * session configured (boot instants, fixtures) there is no schema to judge\n * against, so it reports nothing rather than false-flagging every listing.\n */\nexport function validateCanvasListingQuery(query: ListingQuery): ContentQueryError[] {\n if (contentSession === null) return []\n const collection = playgroundCollection(query.collection)\n if (!collection) {\n return [\n queryError('unknown-collection', `Collection \"${query.collection}\" is not defined.`, [\n 'collection',\n ]),\n ]\n }\n return [\n ...validateWhere(collection, query.where),\n ...(query.sort ? validateSort(collection, query.sort) : []),\n ...(query.limit !== undefined ? validateLimit(query.limit) : []),\n ]\n}\n\n/**\n * Resolve a rich-text internal link's target document to its URL path for the canvas\n * (EC-291) — the studio analog of the host's baked links map. Derives the URL through\n * the active session's client (published perspective, active locale, config-sourced\n * routes), so the canvas previews a `{type:'reference'}` link's `href` exactly as\n * delivery serves it. `null` when the target is unpublished, missing, or addressed by\n * no route → the rich-text renderer shows the EC-290 `unresolved-ref` issue. Reuses\n * the same `resolveLink` path as the host bake, so both agree on the derived URL.\n */\nexport function resolveCanvasLink(\n ref: DocumentRef,\n locale: Locale = getStudioLocale(),\n perspective: CanvasPerspective = 'published',\n): string | null {\n return clientFor(locale, perspective).resolveLink(ref)\n}\n\n/** A pickable entry of a collection (the template-page preview switcher). */\nexport interface CollectionEntrySummary {\n id: string\n label: string\n}\n\n/** Published entries of a collection, labeled by the schema's title field. */\nexport function listCollectionEntries(collectionId: string): CollectionEntrySummary[] {\n const collection = playgroundCollection(collectionId)\n const result = runCollectionListQuery(collectionId)\n if (!result.ok) return []\n // EC-242: label via the shared identity resolver (string `titleField` or a\n // function pattern), over the delivery-shape values; falls back to the id.\n return result.documents.map((doc) => ({\n id: doc.id,\n label: collection ? resolveDocumentTitle(collection, doc, doc.id) : doc.id,\n }))\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import type { OperationEvent } from '@elytracms/studio/lib/operations';
2
+ /**
3
+ * Content-session write-sync (EC-460) — the root-cause fix for the playground
4
+ * population family.
5
+ *
6
+ * The canvas preview engine (`collection-content.ts`) resolves listings,
7
+ * relation picks, and rich-text links against a module-global content session
8
+ * that used to refresh ONLY at project boot and per CMS-surface mount
9
+ * (`project-session.tsx`). A document authored or published after the last of
10
+ * those hydrations was invisible to every canvas resolver while the picker UIs
11
+ * (which read the live CMS store) showed it — the listing rendered its empty
12
+ * state against real published same-session content, and a `multiple` relation
13
+ * resolved 1-of-3 / 0-of-3 picked chips, all silently.
14
+ *
15
+ * This module closes the loop at the ONE write chokepoint the studio already
16
+ * has: the operations lifecycle event bus ("validate → authorize → mutate →
17
+ * emit"; its documented consumers include cache invalidation, vision AD-7 —
18
+ * the reference index and revalidation webhooks attach the same way). Every
19
+ * SUCCEEDED command in a content-bearing domain marks the session stale; a
20
+ * registered refresher (the project session's own `loadCms`, so both modes run
21
+ * the identical `loadHydratedCms` → `configureContentSession` path — no new
22
+ * loader, no transport bypass, no second cache) re-hydrates it, debounced so a
23
+ * persister burst coalesces into one load.
24
+ *
25
+ * Refreshes only run while a CONSUMER (a mounted canvas) is registered — a
26
+ * session with no preview surface just keeps the stale flag and re-hydrates
27
+ * when the next consumer arrives, so connected mode never pays a per-autosave
28
+ * query while nobody is previewing. The pure orchestrator
29
+ * ({@link createContentSessionSync}) takes its timer + refresh injected, so the
30
+ * coalescing/latch behaviour is unit-tested in node (house pattern); the
31
+ * module-level singleton below is the app wiring.
32
+ */
33
+ /** Domains whose succeeded commands change what the preview engine serves. */
34
+ export declare const CONTENT_SESSION_WRITE_DOMAINS: ReadonlySet<string>;
35
+ /** Whether an operation event invalidates the preview content session. */
36
+ export declare function isContentSessionWrite(event: OperationEvent): boolean;
37
+ export interface ContentSessionSyncDeps {
38
+ /** Re-hydrate the session (the project session's `loadCms`). Never throws upward. */
39
+ refresh: () => Promise<unknown>;
40
+ /** Trailing debounce for write bursts (ms). */
41
+ delayMs?: number;
42
+ /** Injectable timer pair (tests drive time manually). */
43
+ setTimer?: (fn: () => void, ms: number) => unknown;
44
+ clearTimer?: (handle: unknown) => void;
45
+ }
46
+ export interface ContentSessionSync {
47
+ /** A content-bearing write succeeded — refresh (debounced) if anyone previews. */
48
+ markStale(): void;
49
+ /** A preview surface mounted; returns its release. A stale session refreshes now. */
50
+ addConsumer(): () => void;
51
+ /** Whether a refresh is currently owed (test/introspection seam). */
52
+ isStale(): boolean;
53
+ }
54
+ /**
55
+ * The pure orchestrator: stale flag + consumer count + one coalesced refresh.
56
+ *
57
+ * - `markStale` with no consumers only flags; the next `addConsumer` refreshes.
58
+ * - `markStale` with consumers schedules a trailing debounce (bursts coalesce).
59
+ * - A `markStale` DURING an in-flight refresh re-runs once after it settles —
60
+ * the refresh that was already reading may have missed that write.
61
+ */
62
+ export declare function createContentSessionSync(deps: ContentSessionSyncDeps): ContentSessionSync;
63
+ type Refresher = () => Promise<unknown>;
64
+ /**
65
+ * Register the active project's session refresher (`ProjectSessionProvider`
66
+ * mounts it with its `loadCms`; leaving the project unregisters). Only one is
67
+ * active at a time — the studio shows one project per session.
68
+ */
69
+ export declare function registerContentSessionRefresher(refresher: Refresher): () => void;
70
+ /** Mark the shared session stale (the event-bus listener calls this). */
71
+ export declare function markContentSessionStale(): void;
72
+ /** Register a mounted preview consumer (the canvas); returns the release. */
73
+ export declare function acquireContentSessionConsumer(): () => void;
74
+ export {};
@@ -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"]}
@@ -2,3 +2,5 @@ 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';
@@ -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': '{id} auswählen',
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',