@eldrajs/theme-vue 0.0.0 → 0.1.0

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/index.js ADDED
@@ -0,0 +1,1049 @@
1
+ import { Fragment, computed, defineAsyncComponent, defineComponent, h, inject, nextTick, onBeforeUnmount, onMounted, onUpdated, provide, reactive, readonly, ref, shallowRef, toValue, watch, watchEffect } from "vue";
2
+ import { EMPTY_THEME_MESSAGES, sanitizeLocaleMessages } from "@eldrajs/theme-core/i18n";
3
+ import { DEFAULT_IMAGE_FRAMING, DEFAULT_LAYOUT_BREAKPOINTS, buildRichTextTree, buildRichTextTree as buildRichTextTree$1, buildTemplateBlockRenames, createLayoutRenderModel, createReusableLayoutRenderModel, createTemplateLayoutRenderModel, designTokenRevisionHash, generateDesignTokenCss, hiddenLayoutBreakpoints, imageFramingAttrs, imageFramingStyle, isBlockFieldLocalized, isBlockFieldLocalized as isBlockFieldLocalized$1, layoutIdentityOf, normalizeImageFraming, normalizeThemeDesignTokens, projectEntryDataLocale, registerBlockFields, restampRichTextPositions, safeHref } from "@eldrajs/theme-core";
4
+ import { linkTargetKeys, resolveLink, resolveLink as resolveLink$1, safeLinkHref } from "@eldrajs/theme-core/links";
5
+ import blockImports from "virtual:eldra/blocks";
6
+ import themeManifest from "virtual:eldra/manifest";
7
+ import themeBreakpoints from "virtual:eldra/breakpoints";
8
+ //#region src/context.ts
9
+ const ELDRA_KEY = Symbol.for("eldra.themeContext");
10
+ /**
11
+ * The preview slice of a fresh theme context, reactive and complete. Every
12
+ * adapter that assembles an `EldraContext` by hand — theme-nuxt's runtime
13
+ * plugin, or an equivalent entry for another framework — must build it from
14
+ * here: the hand-written literal had already drifted several fields behind
15
+ * this interface, and a missing field is a runtime error rather than a type
16
+ * error once the object is assembled elsewhere.
17
+ */
18
+ function createEldraPreviewState() {
19
+ return reactive({
20
+ active: false,
21
+ mode: "preview",
22
+ locale: null,
23
+ sourceDrafts: {},
24
+ drafts: {},
25
+ draftSchemaApiIds: {},
26
+ refreshRevision: 0,
27
+ revision: 0,
28
+ designTokensRevision: 0,
29
+ messagesRevision: 0,
30
+ tokenRevision: 0,
31
+ editorSupportsSlots: false,
32
+ richTextRenderRevision: 0,
33
+ isRichTextRenderDeferred: void 0,
34
+ slotGeometryReporter: void 0
35
+ });
36
+ }
37
+ /**
38
+ * The link slice of a fresh theme context, reactive and empty. Built here for
39
+ * the same reason `createEldraPreviewState` is: a missing field is a runtime
40
+ * error rather than a type error once the object is assembled elsewhere.
41
+ */
42
+ /**
43
+ * The locale slice of a context with no router behind it — a Storybook story, a unit-test mount, a
44
+ * site whose organisation configures no locales. Every answer is the one-unprefixed-site answer,
45
+ * so a block written against `useEldraLocale()` renders identically in all three.
46
+ *
47
+ * An adapter that *has* routing builds its own (see `@eldrajs/theme-nuxt`'s runtime plugin): the
48
+ * active locale and the paths are derived from the route, which this layer knows nothing about.
49
+ */
50
+ function createEldraLocaleState() {
51
+ return reactive({
52
+ active: null,
53
+ defaultLocale: null,
54
+ supported: [],
55
+ name: (locale) => locale,
56
+ path: (href) => href,
57
+ switchPath: () => "/",
58
+ select: () => {}
59
+ });
60
+ }
61
+ function createEldraLinkState() {
62
+ return reactive({
63
+ pages: [],
64
+ templates: [],
65
+ targets: /* @__PURE__ */ new Map()
66
+ });
67
+ }
68
+ function provideEldra(opts) {
69
+ const context = {
70
+ client: opts.client,
71
+ designTokens: reactive(normalizeThemeDesignTokens(opts.designTokens ?? { colors: {} })),
72
+ messages: reactive(opts.messages ?? EMPTY_THEME_MESSAGES),
73
+ links: createEldraLinkState(),
74
+ locales: createEldraLocaleState(),
75
+ preview: createEldraPreviewState()
76
+ };
77
+ provide(ELDRA_KEY, context);
78
+ return context;
79
+ }
80
+ function useEldra() {
81
+ const context = inject(ELDRA_KEY);
82
+ if (context === void 0) throw new Error("[eldra] useEldra() called without provideEldra() in an ancestor");
83
+ return context;
84
+ }
85
+ //#endregion
86
+ //#region src/useEldraLink.ts
87
+ /**
88
+ * Resolve a `link` field's value against the site's own routes.
89
+ *
90
+ * Call it once per component and keep the returned function — it closes over
91
+ * the reactive link state, so every call reads whatever the adapter has filled
92
+ * in by then, and a row re-resolves when a target arrives or a preview draft
93
+ * changes the value.
94
+ *
95
+ * Outside a themed app (a block rendered in isolation, a story with no
96
+ * provider) it resolves nothing: every value comes back `null`, which a block
97
+ * already has to handle for a target that no longer exists.
98
+ *
99
+ * Every href it hands back is spelled in the **active content locale**
100
+ * (`useEldraLocale()`), so a navigation resolved on `/is-IS/about` points at
101
+ * `/is-IS/products/x` and a visitor cannot be dropped back into the default
102
+ * language by following a link. The rewrite is idempotent, which is what lets a
103
+ * theme's router-link component prefix as well without the two compounding.
104
+ */
105
+ function useEldraLink() {
106
+ const context = inject(ELDRA_KEY, null);
107
+ if (context === null || context.links === void 0) return () => null;
108
+ return (value) => localized(resolveLink$1(value, context.links), context.locales);
109
+ }
110
+ /**
111
+ * The resolved link with every same-site href under the active locale, children included.
112
+ *
113
+ * Short-circuits on the two states that need no rewrite — no locale state at all, and a page in
114
+ * the locale served at `/` — because that is nearly every page of nearly every site, and a
115
+ * navigation block resolves one of these per row on every render.
116
+ */
117
+ function localized(link, locales) {
118
+ if (link === null || locales === void 0) return link;
119
+ if (locales.active === null || locales.active === locales.defaultLocale) return link;
120
+ const prefix = locales.path;
121
+ const walk = (one) => ({
122
+ ...one,
123
+ href: one.href === null ? null : prefix(one.href),
124
+ children: one.children.map(walk)
125
+ });
126
+ return walk(link);
127
+ }
128
+ //#endregion
129
+ //#region src/useEldraLocale.ts
130
+ /**
131
+ * The locale this page is in, the locales the site serves, and how to spell a destination in one
132
+ * of them.
133
+ *
134
+ * Call it once per component and keep what it returns — the state is reactive, so a template that
135
+ * reads `active` or calls `path()` re-renders when the visitor changes language.
136
+ *
137
+ * Outside a themed app, and on a site whose organisation configures no locales, it answers the
138
+ * one-unprefixed-site answer: `active` and `defaultLocale` `null`, `supported` empty, `path()` the
139
+ * identity. A block can therefore read it unconditionally and render the same markup in a
140
+ * Storybook story as on a single-locale site — which is also why a language switcher's own rule is
141
+ * `supported.length > 1`, never "is there a locale at all".
142
+ */
143
+ function useEldraLocale() {
144
+ return inject(ELDRA_KEY, null)?.locales ?? INERT;
145
+ }
146
+ /**
147
+ * One shared inert state, not one per call: it is immutable in every way that matters (nothing
148
+ * writes to it) and a block calling this in `setup()` must not allocate a reactive object per
149
+ * instance.
150
+ */
151
+ const INERT = createEldraLocaleState();
152
+ //#endregion
153
+ //#region src/EldraBlockZone.ts
154
+ /** Resolve the schema API id across both supported gateway response shapes. */
155
+ function getBlockSchemaApiId(entry) {
156
+ const direct = entry.schemaApiId;
157
+ if (typeof direct === "string") return direct;
158
+ const schema = entry.schema;
159
+ if (typeof schema === "object" && schema !== null && typeof schema.apiId === "string") return schema.apiId;
160
+ return null;
161
+ }
162
+ const componentCache = /* @__PURE__ */ new Map();
163
+ /** Internal renderer lookup shared by the flat and responsive layout adapters. */
164
+ function getBlockComponent(apiId) {
165
+ const importer = blockImports[apiId];
166
+ if (importer === void 0) return null;
167
+ let component = componentCache.get(apiId);
168
+ if (component === void 0) {
169
+ component = defineAsyncComponent(async () => (await importer()).default);
170
+ componentCache.set(apiId, component);
171
+ }
172
+ return component;
173
+ }
174
+ const EldraBlockZone = defineComponent({
175
+ name: "EldraBlockZone",
176
+ props: { blocks: {
177
+ type: Array,
178
+ required: true
179
+ } },
180
+ setup(props) {
181
+ return () => props.blocks.map((entry) => {
182
+ const apiId = getBlockSchemaApiId(entry);
183
+ const component = apiId === null ? null : getBlockComponent(apiId);
184
+ if (apiId === null || component === null) {
185
+ console.warn(`[eldra] no theme block component for schemaApiId "${String(apiId)}" (entry ${entry.id})`);
186
+ return h("div", {
187
+ key: entry.id,
188
+ style: "display:none",
189
+ "data-eldra-missing-block": apiId ?? "unknown"
190
+ });
191
+ }
192
+ return h("div", {
193
+ key: entry.id,
194
+ "data-eldra-block": entry.id,
195
+ "data-eldra-schema": apiId
196
+ }, [h(component, { entry })]);
197
+ });
198
+ }
199
+ });
200
+ //#endregion
201
+ //#region src/EldraLayout.ts
202
+ /**
203
+ * Marker-mode-only styling for editor slot markers. Injected as a style
204
+ * element only while an editor with the `block-slots` capability is active;
205
+ * static output never emits it, so public layout is unaffected. Markers
206
+ * themselves are inert (pointer-events:none) and aria-hidden.
207
+ */
208
+ const SLOT_MARKER_CSS = [
209
+ ".eldra-slot-marker{",
210
+ "display:inline-block;",
211
+ "max-width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;",
212
+ "font:11px/1.5 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;",
213
+ "color:#3f3f46;background:#fafafa;",
214
+ "border:1px dashed #b6dfff;border-radius:3px;",
215
+ "padding:0 6px;margin:4px 0;",
216
+ "}"
217
+ ].join("");
218
+ /**
219
+ * Module-scope catalog of every block's declared slots, derived once from the
220
+ * theme manifest. Keys are the same apiIds `getBlockSchemaApiId` resolves, so
221
+ * a rendered block can look up the slot ids its author actually declared.
222
+ */
223
+ function buildSlotCatalog() {
224
+ const blocks = themeManifest.blocks ?? [];
225
+ const out = {};
226
+ for (const block of blocks) if (block.apiId && Array.isArray(block.slots)) out[block.apiId] = block.slots;
227
+ return out;
228
+ }
229
+ const SLOT_CATALOG = buildSlotCatalog();
230
+ function buildTemplateBlockCatalog() {
231
+ const blocks = themeManifest.blocks ?? [];
232
+ const out = {};
233
+ for (const block of blocks) {
234
+ if (typeof block.apiId !== "string" || getBlockComponent(block.apiId) === null) continue;
235
+ const renames = buildTemplateBlockRenames(block.migrations);
236
+ out[block.apiId] = {
237
+ apiId: block.apiId,
238
+ fields: (block.fields ?? []).filter((field) => typeof field.fieldId === "string").map((field) => ({
239
+ fieldId: field.fieldId,
240
+ ...Object.hasOwn(field, "default") ? { default: field.default } : {}
241
+ })),
242
+ ...Object.keys(renames).length === 0 ? {} : { renames }
243
+ };
244
+ }
245
+ return out;
246
+ }
247
+ const TEMPLATE_BLOCK_CATALOG = buildTemplateBlockCatalog();
248
+ /**
249
+ * The theme's own tablet/normal layout breakpoints, already resolved and
250
+ * validated by Core (`resolveLayoutBreakpoints`) at build time — this
251
+ * binding only reads it through. Carried by the separate
252
+ * `virtual:eldra/breakpoints` module, not `virtual:eldra/manifest`: the
253
+ * manifest is exactly what is persisted/uploaded, and Core's ingest rejects
254
+ * an unrecognized top-level key on that file. Falls back to the defaults for
255
+ * a build whose plugin predates this virtual module (an unresolved import
256
+ * would throw, not silently omit the field the way an optional manifest key
257
+ * would have).
258
+ */
259
+ const MANIFEST_BREAKPOINTS$1 = themeBreakpoints ?? DEFAULT_LAYOUT_BREAKPOINTS;
260
+ function indexEntries(entries) {
261
+ const indexed = /* @__PURE__ */ new Map();
262
+ for (const entry of entries) if (!indexed.has(entry.id)) indexed.set(entry.id, entry);
263
+ return indexed;
264
+ }
265
+ function renderIdentity(node) {
266
+ return node.renderId ?? node.id;
267
+ }
268
+ function reusablePlacement(node) {
269
+ if (!("placementId" in node)) return void 0;
270
+ return typeof node.placementId === "string" ? node.placementId : void 0;
271
+ }
272
+ /**
273
+ * `data-eldra-hidden` — the breakpoints the layout hides this node at, space
274
+ * separated, or nothing when it is visible everywhere.
275
+ *
276
+ * Rendered unconditionally, in preview and on the published site alike: it
277
+ * carries no styling of its own (the generated layout CSS does the hiding), it
278
+ * is identical on the server and on the client, and it is how the overlay finds
279
+ * the nodes to mark when Studio switches the theme into edit mode. Deciding it
280
+ * per bridge mode instead would be a hydration mismatch.
281
+ */
282
+ function hiddenBreakpoints(style) {
283
+ return hiddenLayoutBreakpoints(style).join(" ") || void 0;
284
+ }
285
+ function renderBlock(node, entries, markerMode) {
286
+ const entry = entries.get(node.entryId);
287
+ const apiId = entry === void 0 ? null : getBlockSchemaApiId(entry);
288
+ const component = apiId === null ? null : getBlockComponent(apiId);
289
+ if (entry === void 0 || apiId === null || component === null) return h("div", {
290
+ key: renderIdentity(node),
291
+ class: node.className,
292
+ hidden: true,
293
+ "data-eldra-layout-node": node.id,
294
+ "data-eldra-hidden": hiddenBreakpoints(node.style),
295
+ "data-eldra-reusable-placement": reusablePlacement(node),
296
+ "data-eldra-missing-block": apiId ?? "unknown"
297
+ });
298
+ const children = [blockWithSlots(node, apiId, entries, markerMode)];
299
+ if (markerMode) for (const def of SLOT_CATALOG[apiId] ?? []) children.push(h("div", {
300
+ key: `slot-marker-${renderIdentity(node)}-${def.id}`,
301
+ class: "eldra-slot-marker",
302
+ "data-eldra-slot-marker": "",
303
+ "data-eldra-slot-id": def.id,
304
+ "data-eldra-layout-node-id": node.id,
305
+ "aria-hidden": "true",
306
+ style: "pointer-events:none"
307
+ }, `${def.label} · ${node.slots?.[def.id]?.length ?? 0}/${def.maxItems}`));
308
+ return h("div", {
309
+ key: renderIdentity(node),
310
+ class: node.className,
311
+ "data-eldra-layout-node": node.id,
312
+ "data-eldra-hidden": hiddenBreakpoints(node.style),
313
+ "data-eldra-reusable-placement": reusablePlacement(node),
314
+ "data-eldra-block": entry.id,
315
+ "data-eldra-schema": apiId
316
+ }, children);
317
+ }
318
+ function renderSlotChildren(host, slotId, slotChildren, entries, markerMode) {
319
+ const hostIdentity = renderIdentity(host);
320
+ return slotChildren.map((child) => {
321
+ const identity = [
322
+ hostIdentity,
323
+ slotId,
324
+ renderIdentity(child)
325
+ ].join("\0");
326
+ return h("div", {
327
+ key: identity,
328
+ class: child.className,
329
+ "data-eldra-layout-node": child.id,
330
+ "data-eldra-hidden": hiddenBreakpoints(child.style),
331
+ "data-eldra-slot-id": slotId,
332
+ "data-eldra-block": child.entryId
333
+ }, [renderBlock(child, entries, markerMode)]);
334
+ });
335
+ }
336
+ function blockWithSlots(host, apiId, entries, markerMode) {
337
+ const declared = SLOT_CATALOG[apiId] ?? [];
338
+ const named = {};
339
+ for (const def of declared) {
340
+ const children = host.slots?.[def.id];
341
+ if (children === void 0 || children.length === 0) continue;
342
+ named[def.id] = () => renderSlotChildren(host, def.id, children, entries, markerMode);
343
+ }
344
+ const entry = entries.get(host.entryId);
345
+ const component = getBlockComponent(apiId);
346
+ return h(component, { entry }, named);
347
+ }
348
+ function renderNode(node, entries, markerMode) {
349
+ if (node.type === "template-block") {
350
+ const component = getBlockComponent(node.apiId);
351
+ if (component === null) return h("div", {
352
+ hidden: true,
353
+ "data-eldra-invalid-layout": ""
354
+ });
355
+ return h("div", {
356
+ key: `${node.id}\u0000${node.entry.id}`,
357
+ class: node.className,
358
+ "data-eldra-layout-node": node.id,
359
+ "data-eldra-template-block": node.id,
360
+ "data-eldra-block": node.entry.id,
361
+ "data-eldra-schema": node.apiId
362
+ }, [h(component, { entry: node.entry })]);
363
+ }
364
+ if (node.type === "block") return renderBlock(node, entries, markerMode);
365
+ return h("div", {
366
+ key: renderIdentity(node),
367
+ class: node.className,
368
+ "data-eldra-layout-node": node.id,
369
+ "data-eldra-hidden": hiddenBreakpoints(node.style),
370
+ "data-eldra-reusable-placement": reusablePlacement(node),
371
+ "data-eldra-layout-container": node.type
372
+ }, node.children.map((child) => renderNode(child, entries, markerMode)));
373
+ }
374
+ /**
375
+ * Render a validated responsive layout with the same model and stylesheet used
376
+ * by static generation. Invalid documents and unresolved entry references fail
377
+ * closed before any customer block component is mounted.
378
+ */
379
+ const EldraLayout = defineComponent({
380
+ name: "EldraLayout",
381
+ props: {
382
+ layout: {
383
+ type: null,
384
+ required: true
385
+ },
386
+ blocks: {
387
+ type: Array,
388
+ required: true
389
+ },
390
+ nonce: {
391
+ type: String,
392
+ required: false,
393
+ default: void 0
394
+ },
395
+ designTokens: {
396
+ type: null,
397
+ required: false,
398
+ default: void 0
399
+ },
400
+ reusableComponentProjection: {
401
+ type: null,
402
+ required: false,
403
+ default: void 0
404
+ },
405
+ templateEntry: {
406
+ type: null,
407
+ required: false,
408
+ default: void 0
409
+ }
410
+ },
411
+ setup(props) {
412
+ const context = inject(ELDRA_KEY, void 0);
413
+ const markerMode = computed(() => context?.preview.active === true && context?.preview.editorSupportsSlots === true);
414
+ const resolvedTokens = () => props.designTokens !== void 0 ? normalizeThemeDesignTokens(props.designTokens) : context?.designTokens ?? normalizeThemeDesignTokens({ colors: {} });
415
+ const state = computed(() => {
416
+ context?.preview.revision;
417
+ const entries = indexEntries(props.blocks.map((entry) => {
418
+ const draft = context?.preview.drafts[entry.id];
419
+ return draft === void 0 ? entry : {
420
+ ...entry,
421
+ data: draft
422
+ };
423
+ }));
424
+ try {
425
+ const tokens = resolvedTokens();
426
+ if (props.templateEntry !== void 0) {
427
+ const draft = context?.preview.drafts[props.templateEntry.id];
428
+ const entry = draft === void 0 ? props.templateEntry : {
429
+ ...props.templateEntry,
430
+ data: draft
431
+ };
432
+ return {
433
+ model: createTemplateLayoutRenderModel(props.layout, {
434
+ entry,
435
+ blockEntries: [...entries.values()],
436
+ blockCatalog: TEMPLATE_BLOCK_CATALOG,
437
+ allowedContainerIds: new Set(Object.keys(tokens.containers)),
438
+ breakpoints: MANIFEST_BREAKPOINTS$1,
439
+ reusableComponentProjection: props.reusableComponentProjection ?? {
440
+ bindings: [],
441
+ revisions: []
442
+ }
443
+ }),
444
+ entries
445
+ };
446
+ }
447
+ const allowedEntryIds = new Set(entries.keys());
448
+ const allowedContainerIds = new Set(Object.keys(tokens.containers));
449
+ const reusable = props.reusableComponentProjection;
450
+ const layoutVersion = typeof props.layout === "object" && props.layout !== null ? props.layout.version : void 0;
451
+ const slotContext = {
452
+ slotCatalog: SLOT_CATALOG,
453
+ entryApiId: (entryId) => {
454
+ const entry = entries.get(entryId);
455
+ return entry === void 0 ? void 0 : getBlockSchemaApiId(entry) ?? void 0;
456
+ }
457
+ };
458
+ return {
459
+ model: reusable === void 0 && layoutVersion !== 2 && layoutVersion !== 3 ? createLayoutRenderModel(props.layout, allowedEntryIds, allowedContainerIds, slotContext, MANIFEST_BREAKPOINTS$1) : createReusableLayoutRenderModel(props.layout, {
460
+ projection: reusable ?? {
461
+ bindings: [],
462
+ revisions: []
463
+ },
464
+ allowedEntryIds,
465
+ allowedContainerIds,
466
+ slotContext,
467
+ breakpoints: MANIFEST_BREAKPOINTS$1
468
+ }),
469
+ entries
470
+ };
471
+ } catch {
472
+ return {
473
+ model: null,
474
+ entries
475
+ };
476
+ }
477
+ });
478
+ const reportSlotMarkers = async () => {
479
+ if (!markerMode.value) return;
480
+ await nextTick();
481
+ const reporter = context?.preview.slotGeometryReporter;
482
+ if (reporter === void 0) return;
483
+ const elements = document.querySelectorAll("[data-eldra-slot-marker]");
484
+ if (elements.length === 0) return;
485
+ const slots = [];
486
+ for (const element of elements) {
487
+ const rect = element.getBoundingClientRect();
488
+ slots.push({
489
+ layoutNodeId: element.getAttribute("data-eldra-layout-node-id") ?? "",
490
+ slotId: element.getAttribute("data-eldra-slot-id") ?? "",
491
+ rect: {
492
+ x: rect.x,
493
+ y: rect.y,
494
+ width: rect.width,
495
+ height: rect.height
496
+ }
497
+ });
498
+ }
499
+ reporter(slots);
500
+ };
501
+ onMounted(() => {
502
+ reportSlotMarkers();
503
+ });
504
+ onUpdated(() => {
505
+ reportSlotMarkers();
506
+ });
507
+ return () => {
508
+ const { model, entries } = state.value;
509
+ if (model === null) return h("div", {
510
+ hidden: true,
511
+ "data-eldra-invalid-layout": ""
512
+ });
513
+ return h(Fragment, [
514
+ h("style", {
515
+ "data-eldra-design-token-styles": "",
516
+ "data-eldra-design-token-revision": designTokenRevisionHash(resolvedTokens()),
517
+ nonce: props.nonce
518
+ }, generateDesignTokenCss(resolvedTokens())),
519
+ h("style", {
520
+ "data-eldra-layout-styles": "",
521
+ nonce: props.nonce
522
+ }, model.css),
523
+ markerMode.value ? h("style", {
524
+ "data-eldra-slot-marker-styles": "",
525
+ nonce: props.nonce
526
+ }, SLOT_MARKER_CSS) : null,
527
+ renderNode(model.root, entries, markerMode.value)
528
+ ]);
529
+ };
530
+ }
531
+ });
532
+ //#endregion
533
+ //#region src/richTextRender.ts
534
+ /**
535
+ * Render a TipTap JSON document's content as Vue VNodes. Non-doc input renders
536
+ * as an empty root.
537
+ *
538
+ * `generation` changes the key prefix, which makes Vue discard the subtree and
539
+ * build it again instead of patching. §18 v3's deferred rendering needs that
540
+ * exactly once per deferral: while a field is deferred the browser edits the
541
+ * DOM behind Vue's back, so the vnode tree Vue is holding no longer describes
542
+ * what is on screen, and an ordinary patch can skip a change it believes is
543
+ * already applied. Generation 0 keeps the original keys, so nothing about the
544
+ * ordinary render path changes.
545
+ */
546
+ function renderRichTextDoc(doc, options, generation = 0) {
547
+ return mapChildren(buildRichTextTree$1(doc, options), generation === 0 ? "rt" : `rt${generation}`);
548
+ }
549
+ function mapChildren(children, key) {
550
+ return children.map((child, index) => typeof child === "string" ? child : mapNode(child, `${key}-${index}`));
551
+ }
552
+ function mapNode(node, key) {
553
+ if (node.children.length === 0) return h(node.tag, {
554
+ key,
555
+ ...node.attrs
556
+ });
557
+ return h(node.tag, {
558
+ key,
559
+ ...node.attrs
560
+ }, mapChildren(node.children, key));
561
+ }
562
+ //#endregion
563
+ //#region src/useEldraBlockField.ts
564
+ /**
565
+ * Vue-facing name for theme-core's block-field lookup. The registry and the
566
+ * `localized` resolution live in `@eldrajs/theme-core` so every framework
567
+ * binding shares one implementation; this wrapper exists only so themes keep
568
+ * calling a composable-shaped API.
569
+ *
570
+ * There is no `toolbar` here any more: §18 v3 keeps the rich-text toolbar in
571
+ * Studio, which reads `metadata.toolbar` from Core's field definitions. The
572
+ * vite plugin still validates the control ids at build time.
573
+ */
574
+ function useEldraBlockField(apiId, fieldId) {
575
+ return { localized: isBlockFieldLocalized$1(apiId, fieldId) };
576
+ }
577
+ //#endregion
578
+ //#region src/EldraRichText.ts
579
+ /**
580
+ * Renders a §18 rich-text field's TipTap JSON document. Read render only —
581
+ * the theme SDK is a bridge, not an editor: it hosts no editing UI and
582
+ * depends on neither TipTap nor a component library.
583
+ *
584
+ * §18 v3: the operator edits this render natively — theme-core's overlay
585
+ * makes the root `contenteditable` in edit mode and reports selection, text
586
+ * ops and commands in ProseMirror document positions; Studio owns the one
587
+ * headless editor and the docked toolbar and echoes the document back
588
+ * through `editor:content-update`. While Studio is editing, the root is
589
+ * marked `data-eldra-rich-text-editing`; nothing is hidden or overlaid.
590
+ *
591
+ * The root carries the marking attributes the overlay's
592
+ * `decorateStegaTextNodes`/`applyEditable`/`reconcileExternalDrafts`
593
+ * recognize (`[data-eldra-rich-text]`) so the overlay skips per-leaf stega
594
+ * decoration here and hands click-to-select to `theme:field-clicked`
595
+ * instead (see theme-core's `overlay.ts`).
596
+ *
597
+ * Per-block toolbar: the `metadata.toolbar` data path is unchanged
598
+ * (`block.json` → manifest → Core field metadata → Studio), but the theme no
599
+ * longer reads it — Studio's docked toolbar does. The vite plugin's scanner
600
+ * still validates the control ids at build time.
601
+ *
602
+ * Locale: an omitted `locale` prop defaults to the preview's active content
603
+ * locale when the manifest marks the field `localized`, and to `null`
604
+ * otherwise. That resolved locale is what `data-eldra-locale` carries, so
605
+ * Studio edits the locale the render actually used.
606
+ *
607
+ * Heading outline: `minHeadingLevel` floors every heading tag at render time,
608
+ * so one stored document fits under two different page outlines without being
609
+ * rewritten (see that prop's own comment).
610
+ */
611
+ const EldraRichText = defineComponent({
612
+ name: "EldraRichText",
613
+ props: {
614
+ entryId: {
615
+ type: String,
616
+ required: true
617
+ },
618
+ field: {
619
+ type: String,
620
+ required: true
621
+ },
622
+ locale: {
623
+ type: String,
624
+ required: false,
625
+ default: null
626
+ },
627
+ doc: {
628
+ type: null,
629
+ required: true
630
+ },
631
+ /** The owning block's schema id — a block component passes `getBlockSchemaApiId(entry)`.
632
+ * Used to resolve the field's `localized` flag from the block manifest. */
633
+ apiId: {
634
+ type: String,
635
+ required: false,
636
+ default: void 0
637
+ },
638
+ /**
639
+ * A floor for every heading in the document: a `heading` node renders as
640
+ * `h{max(minHeadingLevel, level)}`, still capped at 6. Defaults to 1 — the
641
+ * generic clamp, i.e. whatever level the document carries.
642
+ *
643
+ * A page owns its heading outline; a rich-text field does not. The same body
644
+ * document is legitimately an h2-and-down section in one block and an
645
+ * h3-and-down one in another (the starter's `article` body starts at h2, its
646
+ * `rich-text` block at h3), and nothing in the document or in `block.json`
647
+ * can say so — `metadata.toolbar`'s `heading` control id is one
648
+ * level-agnostic entry, so an editor can insert any level anywhere. Without
649
+ * this prop the only place to enforce it was a per-block transform over the
650
+ * TipTap JSON before handing it in (the starter shipped one); doing it here
651
+ * instead means the **document is never rewritten** — no copy, no
652
+ * reactive-state surgery, and an editor's own level survives a round trip
653
+ * through Studio untouched, because only the rendered tag moves.
654
+ *
655
+ * A floor, never an offset: an author who already wrote h3 under
656
+ * `minHeadingLevel: 3` keeps h3, and h4 stays h4 — only levels above the
657
+ * floor move. Every guard is in `@eldrajs/theme-core`'s own
658
+ * `clampHeadingLevel`, so an out-of-range or non-integer value can never
659
+ * produce an `<h0>`/`<h7>`.
660
+ *
661
+ * Rendering-only, deliberately: the value takes no part in position
662
+ * stamping (`data-eldra-pos` is the document's own numbering, which a
663
+ * rendered tag name cannot change), so native editing, selection reporting
664
+ * and `restampRichTextPositions` all behave exactly as they do without it.
665
+ */
666
+ minHeadingLevel: {
667
+ type: Number,
668
+ required: false,
669
+ default: 1
670
+ }
671
+ },
672
+ setup(props, { expose }) {
673
+ const context = inject(ELDRA_KEY, null);
674
+ const apiId = props.apiId ?? void 0;
675
+ const blockField = useEldraBlockField(apiId, props.field);
676
+ if (apiId === void 0 && props.locale === null && context !== null) {
677
+ let warned = false;
678
+ watchEffect(() => {
679
+ if (warned) return;
680
+ if (context.preview.mode !== "edit" || context.preview.locale === null) return;
681
+ warned = true;
682
+ console.warn(`[eldra] EldraRichText: field "${props.field}" has neither an \`apiId\` nor a \`locale\` prop, so locale resolution is skipped and it reports \`locale: null\` while the preview is on "${context.preview.locale}". Pass \`api-id\` (getBlockSchemaApiId(entry)) or an explicit \`locale\`.`);
683
+ });
684
+ }
685
+ const locale = computed(() => props.locale ?? (blockField.localized ? context?.preview.locale ?? null : null));
686
+ expose({ locale });
687
+ /**
688
+ * §18 v3 deferred rendering. While the operator types natively in this
689
+ * root, the browser owns its DOM and theme-core says so
690
+ * (`isRichTextRenderDeferred`); replacing the tree would take the caret
691
+ * with it. So the component renders from `rendered` rather than straight
692
+ * from the prop, and while the field is deferred a new document only
693
+ * re-stamps the positions in place.
694
+ *
695
+ * Every decision here is core's: this is the predicate, the counter that
696
+ * makes it reactive, and the `renderKey` bump that guarantees Vue rebuilds
697
+ * the subtree once the field comes back — its vnode tree is stale by then,
698
+ * because the browser edited the DOM behind it, so an ordinary patch could
699
+ * skip a change it wrongly believes is already applied.
700
+ */
701
+ const root = shallowRef(null);
702
+ const rendered = shallowRef(props.doc);
703
+ const renderKey = shallowRef(0);
704
+ let domOwnedByBrowser = false;
705
+ watch([() => props.doc, () => context?.preview.richTextRenderRevision ?? 0], () => {
706
+ if (context?.preview.isRichTextRenderDeferred?.({
707
+ entryId: props.entryId,
708
+ fieldPath: props.field,
709
+ locale: locale.value,
710
+ ...layoutIdentityOf(root.value)
711
+ }) === true) {
712
+ domOwnedByBrowser = true;
713
+ if (root.value !== null && restampRichTextPositions(root.value, props.doc)) return;
714
+ }
715
+ if (domOwnedByBrowser) {
716
+ renderKey.value += 1;
717
+ domOwnedByBrowser = false;
718
+ }
719
+ rendered.value = props.doc;
720
+ }, { flush: "post" });
721
+ return () => h("div", {
722
+ ref: root,
723
+ "data-eldra-rich-text": "",
724
+ "data-eldra-field": props.field,
725
+ "data-eldra-entry": props.entryId,
726
+ ...locale.value ? { "data-eldra-locale": locale.value } : {}
727
+ }, renderRichTextDoc(rendered.value, {
728
+ safeHref,
729
+ padEmptyBlocks: context?.preview.mode === "edit",
730
+ minHeadingLevel: props.minHeadingLevel
731
+ }, renderKey.value));
732
+ }
733
+ });
734
+ //#endregion
735
+ //#region src/useEldraEntry.ts
736
+ function useEldraEntry(opts) {
737
+ const context = useEldra();
738
+ const entry = ref(null);
739
+ const pending = ref(false);
740
+ const error = ref(null);
741
+ let requestVersion = 0;
742
+ function overlayDraft() {
743
+ const current = entry.value;
744
+ if (current === null) return;
745
+ const draft = context.preview.drafts[current.id];
746
+ if (draft !== void 0) entry.value = {
747
+ ...current,
748
+ data: draft
749
+ };
750
+ }
751
+ async function refresh() {
752
+ const version = ++requestVersion;
753
+ pending.value = true;
754
+ error.value = null;
755
+ try {
756
+ const id = toValue(opts.entryId);
757
+ let result;
758
+ if (id !== void 0) result = await context.client.getEntry(opts.schemaApiId, id, opts.query);
759
+ else if (opts.uniqueField !== void 0) result = await context.client.getEntryByUniqueField(opts.schemaApiId, opts.uniqueField.fieldId, toValue(opts.uniqueField.value), opts.query);
760
+ else result = null;
761
+ if (version !== requestVersion) return;
762
+ entry.value = result;
763
+ overlayDraft();
764
+ } catch (cause) {
765
+ if (version !== requestVersion) return;
766
+ error.value = cause instanceof Error ? cause.message : String(cause);
767
+ } finally {
768
+ if (version === requestVersion) pending.value = false;
769
+ }
770
+ }
771
+ watch(() => [toValue(opts.entryId), opts.uniqueField === void 0 ? void 0 : toValue(opts.uniqueField.value)], () => {
772
+ refresh();
773
+ }, { immediate: true });
774
+ watch(() => context.preview.revision, overlayDraft);
775
+ return {
776
+ entry,
777
+ pending,
778
+ error,
779
+ refresh
780
+ };
781
+ }
782
+ //#endregion
783
+ //#region src/useEldraPreview.ts
784
+ /**
785
+ * The theme's own tablet/normal layout breakpoints, already resolved and
786
+ * validated by Core (`resolveLayoutBreakpoints`) at build time — included in
787
+ * every `theme:ready` (see below) so Studio's UI can react to the theme's
788
+ * actual ranges rather than a value it may not share. Carried by the
789
+ * separate `virtual:eldra/breakpoints` module, not `virtual:eldra/manifest`:
790
+ * the manifest is exactly what is persisted/uploaded, and Core's ingest
791
+ * rejects an unrecognized top-level key on that file.
792
+ */
793
+ const MANIFEST_BREAKPOINTS = themeBreakpoints ?? DEFAULT_LAYOUT_BREAKPOINTS;
794
+ /**
795
+ * Bounds theme:slots-rendered reports, mirroring Studio's sanitizeRenderedBlocks
796
+ * (contracts C14): at most 500 entries, finite rect numbers, and width/height
797
+ * within 0..100_000. The editor-side sanitizer is the authority; stay in sync.
798
+ */
799
+ function sanitizeSlotGeometry(slots) {
800
+ if (!Array.isArray(slots)) return [];
801
+ const out = [];
802
+ for (const candidate of slots.slice(0, 500)) {
803
+ if (candidate === null || typeof candidate !== "object") continue;
804
+ const entry = candidate;
805
+ if (typeof entry.layoutNodeId !== "string" || entry.layoutNodeId.length > 128) continue;
806
+ if (typeof entry.slotId !== "string" || entry.slotId.length > 128) continue;
807
+ const rect = entry.rect;
808
+ if (rect === null || typeof rect !== "object") continue;
809
+ const r = rect;
810
+ const x = r.x;
811
+ const y = r.y;
812
+ const width = r.width;
813
+ const height = r.height;
814
+ if (typeof x !== "number" || !Number.isFinite(x)) continue;
815
+ if (typeof y !== "number" || !Number.isFinite(y)) continue;
816
+ if (typeof width !== "number" || !Number.isFinite(width) || width < 0 || width > 1e5) continue;
817
+ if (typeof height !== "number" || !Number.isFinite(height) || height < 0 || height > 1e5) continue;
818
+ out.push({
819
+ layoutNodeId: entry.layoutNodeId,
820
+ slotId: entry.slotId,
821
+ rect: {
822
+ x,
823
+ y,
824
+ width,
825
+ height
826
+ }
827
+ });
828
+ }
829
+ return out;
830
+ }
831
+ /**
832
+ * Exported for `previewDrafts.spec.ts`: proving a draft's own top-level
833
+ * select field unwraps the same way a select nested in an embedded block
834
+ * does (SF-2) needs to call this directly, without standing up the whole
835
+ * `startEldraPreview` bridge/overlay runtime that owns every other call site.
836
+ */
837
+ function projectDrafts(context) {
838
+ for (const [entryId, draftDoc] of Object.entries(context.preview.sourceDrafts)) context.preview.drafts[entryId] = context.client.encodeEntryDataStega(entryId, projectEntryDataLocale(draftDoc, context.preview.locale, context.preview.draftSchemaApiIds[entryId]), context.preview.locale, context.preview.draftSchemaApiIds[entryId]);
839
+ }
840
+ function startEldraPreview(context, opts) {
841
+ const active = ref(false);
842
+ if (typeof window === "undefined" || window.parent === window) return {
843
+ active: readonly(active),
844
+ destroy: () => void 0
845
+ };
846
+ let destroyRuntime = null;
847
+ let disposed = false;
848
+ (async () => {
849
+ const [{ createThemeBridge }, { createOverlayRuntime, createPreviewMessageRouter }] = await Promise.all([import("@eldrajs/theme-core/bridge"), import("@eldrajs/theme-core/overlay")]);
850
+ if (disposed) return;
851
+ let overlay;
852
+ /** The preview token the last `editor:init` carried — see `tokenRevision`. */
853
+ let lastPreviewToken = null;
854
+ const router = createPreviewMessageRouter(() => overlay);
855
+ const bridge = createThemeBridge({
856
+ allowedOrigins: opts.allowedOrigins,
857
+ breakpoints: MANIFEST_BREAKPOINTS,
858
+ onEditorHello(capabilities) {
859
+ context.preview.editorSupportsSlots = router.negotiate(capabilities).slots;
860
+ },
861
+ onMessage(type, payload) {
862
+ if (router.route(type, payload)) return;
863
+ if (type === "editor:init") {
864
+ const init = payload;
865
+ context.client.enablePreview(init.previewToken);
866
+ if (init.previewToken !== lastPreviewToken) {
867
+ lastPreviewToken = init.previewToken;
868
+ context.preview.tokenRevision += 1;
869
+ }
870
+ context.preview.active = true;
871
+ context.preview.mode = init.mode;
872
+ context.preview.locale = init.locale;
873
+ projectDrafts(context);
874
+ active.value = true;
875
+ overlay.setMode(init.mode);
876
+ overlay.start();
877
+ context.preview.refreshRevision += 1;
878
+ context.preview.revision += 1;
879
+ if (init.path !== window.location.pathname) opts.onNavigate?.(init.path);
880
+ } else if (type === "editor:content-update") {
881
+ const entries = payload.entries;
882
+ overlay.acceptExternalUpdate(entries.map(({ entryId }) => entryId));
883
+ for (const update of entries) {
884
+ context.preview.sourceDrafts[update.entryId] = update.draftDoc;
885
+ context.preview.draftSchemaApiIds[update.entryId] = update.schemaApiId;
886
+ }
887
+ projectDrafts(context);
888
+ context.preview.refreshRevision += 1;
889
+ context.preview.revision += 1;
890
+ queueMicrotask(() => {
891
+ overlay.rescan();
892
+ overlay.reconcileExternalDrafts(context.preview.drafts, entries.map(({ entryId }) => entryId));
893
+ });
894
+ } else if (type === "editor:design-tokens") applyResolvedDesignTokens(context, payload);
895
+ else if (type === "editor:theme-messages") applyThemeMessages(context, payload);
896
+ else if (type === "editor:set-mode") {
897
+ const mode = payload.mode;
898
+ context.preview.mode = mode;
899
+ overlay.setMode(mode);
900
+ } else if (type === "editor:navigate") opts.onNavigate?.(payload.path);
901
+ else if (type === "editor:refresh") context.preview.refreshRevision += 1;
902
+ }
903
+ });
904
+ overlay = createOverlayRuntime({
905
+ post: (type, payload) => bridge.post(type, payload),
906
+ breakpoints: MANIFEST_BREAKPOINTS
907
+ });
908
+ context.preview.slotGeometryReporter = (slots) => {
909
+ const safe = sanitizeSlotGeometry(slots);
910
+ if (safe.length === 0) return;
911
+ bridge.post("theme:slots-rendered", { slots: safe });
912
+ };
913
+ context.preview.isRichTextRenderDeferred = (identity) => overlay.isRichTextRenderDeferred(identity);
914
+ const stopRichTextRenderState = overlay.onRichTextRenderState(() => {
915
+ context.preview.richTextRenderRevision += 1;
916
+ });
917
+ const stopRequestErrors = context.client.onRequestError?.((error) => {
918
+ if (!context.client.previewEnabled || error.status === 404) return;
919
+ bridge.post("theme:request-failed", {
920
+ status: error.status,
921
+ path: error.path.slice(0, 256)
922
+ });
923
+ }) ?? (() => void 0);
924
+ destroyRuntime = () => {
925
+ context.client.disablePreview();
926
+ context.preview.active = false;
927
+ context.preview.editorSupportsSlots = false;
928
+ context.preview.slotGeometryReporter = void 0;
929
+ stopRichTextRenderState();
930
+ stopRequestErrors();
931
+ context.preview.isRichTextRenderDeferred = void 0;
932
+ active.value = false;
933
+ router.reset();
934
+ overlay.stop();
935
+ bridge.destroy();
936
+ document.querySelector("style[data-eldra-live-design-token-styles]")?.remove();
937
+ };
938
+ })();
939
+ return {
940
+ active: readonly(active),
941
+ destroy: () => {
942
+ disposed = true;
943
+ destroyRuntime?.();
944
+ }
945
+ };
946
+ }
947
+ function boundedPayload(value) {
948
+ try {
949
+ return new TextEncoder().encode(JSON.stringify(value)).byteLength <= 65536;
950
+ } catch {
951
+ return false;
952
+ }
953
+ }
954
+ function replaceRecord(target, source) {
955
+ for (const key of Object.keys(target)) delete target[key];
956
+ for (const key of Object.keys(source).sort((a, b) => a.localeCompare(b))) target[key] = source[key];
957
+ }
958
+ const staticDesignTokenIds = /* @__PURE__ */ new WeakMap();
959
+ function getStaticDesignTokenIds(context) {
960
+ const existing = staticDesignTokenIds.get(context);
961
+ if (existing !== void 0) return existing;
962
+ const captured = {
963
+ colors: Object.keys(context.designTokens.colors),
964
+ containers: Object.keys(context.designTokens.containers)
965
+ };
966
+ staticDesignTokenIds.set(context, captured);
967
+ return captured;
968
+ }
969
+ function applyResolvedDesignTokens(context, update) {
970
+ if (!boundedPayload(update)) return false;
971
+ try {
972
+ const resolved = normalizeThemeDesignTokens(update.resolved);
973
+ if (!Number.isSafeInteger(update.revision) || update.revision <= context.preview.designTokensRevision) return false;
974
+ const staticIds = getStaticDesignTokenIds(context);
975
+ const removedColors = staticIds.colors.filter((id) => resolved.colors[id] === void 0);
976
+ const removedContainers = staticIds.containers.filter((id) => resolved.containers[id] === void 0);
977
+ replaceRecord(context.designTokens.colors, resolved.colors);
978
+ replaceRecord(context.designTokens.containers, resolved.containers);
979
+ if (resolved.allowCustomColors === true) context.designTokens.allowCustomColors = true;
980
+ else delete context.designTokens.allowCustomColors;
981
+ context.preview.designTokensRevision = update.revision;
982
+ if (typeof document !== "undefined") syncDesignTokenStyle(context, {
983
+ colors: removedColors,
984
+ containers: removedContainers
985
+ });
986
+ return true;
987
+ } catch {
988
+ return false;
989
+ }
990
+ }
991
+ /**
992
+ * Applies an `editor:theme-messages` push from Studio's "Theme texts" page: for every locale tag
993
+ * the payload names, **replaces** `context.messages.locales[tag]` wholesale with the payload's own
994
+ * record (sanitized through `@eldrajs/theme-core/i18n`'s `sanitizeLocaleMessages` — non-string
995
+ * values dropped, and a forbidden tag, `__proto__`/`prototype`/`constructor`, skipped entirely
996
+ * rather than written through: `context.messages` is `reactive(...)`, so an assignment to that
997
+ * literal key on its `locales` object would reassign the live, shared object's own prototype
998
+ * instead of adding a property named for it) — never a per-key merge, since Studio always sends a
999
+ * locale's full resolved set, the same shape `virtual:eldra/messages` serves. A locale the payload
1000
+ * does not mention is left untouched.
1001
+ *
1002
+ * `revision` guards the same way `designTokensRevision` does: a push whose revision is not
1003
+ * strictly greater than the last one applied is ignored, so an out-of-order delivery (a Studio
1004
+ * reconnect replaying its last push, say) cannot roll a live edit backwards.
1005
+ */
1006
+ function applyThemeMessages(context, update) {
1007
+ if (!boundedPayload(update)) return false;
1008
+ if (!Number.isSafeInteger(update.revision) || update.revision <= context.preview.messagesRevision) return false;
1009
+ if (typeof update.locales !== "object" || update.locales === null) return false;
1010
+ for (const tag of Object.keys(update.locales).sort((a, b) => a.localeCompare(b))) {
1011
+ const record = update.locales[tag];
1012
+ if (typeof record !== "object" || record === null) continue;
1013
+ const sanitized = sanitizeLocaleMessages(tag, record);
1014
+ if (sanitized === null) continue;
1015
+ context.messages.locales[tag] = sanitized;
1016
+ }
1017
+ context.preview.messagesRevision = update.revision;
1018
+ return true;
1019
+ }
1020
+ function syncDesignTokenStyle(context, removed) {
1021
+ let style = document.querySelector("style[data-eldra-live-design-token-styles]");
1022
+ if (style === null) {
1023
+ style = document.createElement("style");
1024
+ style.dataset.eldraLiveDesignTokenStyles = "";
1025
+ const nonce = document.querySelector("style[data-eldra-layout-styles][nonce]")?.nonce;
1026
+ if (nonce) style.nonce = nonce;
1027
+ document.head.append(style);
1028
+ }
1029
+ const removedDeclarations = [...removed.colors.sort((a, b) => a.localeCompare(b)).flatMap((id) => [`--eldra-color-${id}:initial;`, `--color-${id}:initial;`]), ...removed.containers.sort((a, b) => a.localeCompare(b)).flatMap((id) => [
1030
+ `--eldra-container-${id}-max-width:initial;`,
1031
+ `--eldra-container-${id}-gutter-normal:initial;`,
1032
+ `--eldra-container-${id}-gutter-tablet:initial;`,
1033
+ `--eldra-container-${id}-gutter-mobile:initial;`
1034
+ ])].join("");
1035
+ const tombstones = removedDeclarations === "" ? "" : `:root{${removedDeclarations}}`;
1036
+ style.textContent = `${tombstones}${generateDesignTokenCss(context.designTokens)}`;
1037
+ style.dataset.eldraDesignTokenRevision = String(context.preview.designTokensRevision);
1038
+ }
1039
+ function useEldraPreview(opts) {
1040
+ const active = ref(false);
1041
+ if (typeof window === "undefined" || window.parent === window) return { active: readonly(active) };
1042
+ const runtime = startEldraPreview(useEldra(), opts);
1043
+ onBeforeUnmount(() => {
1044
+ runtime.destroy();
1045
+ });
1046
+ return { active: runtime.active };
1047
+ }
1048
+ //#endregion
1049
+ export { DEFAULT_IMAGE_FRAMING, ELDRA_KEY, EldraBlockZone, EldraLayout, EldraRichText, buildRichTextTree, createEldraLinkState, createEldraLocaleState, createEldraPreviewState, getBlockSchemaApiId, imageFramingAttrs, imageFramingStyle, isBlockFieldLocalized, linkTargetKeys, normalizeImageFraming, provideEldra, registerBlockFields, renderRichTextDoc, resolveLink, safeLinkHref, startEldraPreview, useEldra, useEldraBlockField, useEldraEntry, useEldraLink, useEldraLocale, useEldraPreview };