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