@b10cks/client 1.4.0 → 1.5.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/README.md CHANGED
@@ -241,6 +241,96 @@ const xml = renderSitemapXml(filtered, 'https://example.com')
241
241
  const index = renderSitemapIndex(['/sitemap-en.xml', '/sitemap-de.xml'], 'https://example.com')
242
242
  ```
243
243
 
244
+ ## Preview bridge & visual editing
245
+
246
+ When a page is rendered inside the b10cks visual editor (in an `<iframe>`), the SDK exchanges `postMessage` events with the editor so blocks can be selected, hovered, and live-updated while editing. These are the framework-agnostic building blocks — the `@b10cks/vue`, `@b10cks/react`, `@b10cks/svelte`, `@b10cks/nuxt`, and `@b10cks/next` packages wrap them in idiomatic directives/hooks/actions.
247
+
248
+ ### `previewBridge`
249
+
250
+ A singleton that bridges the preview iframe and the editor.
251
+
252
+ ```typescript
253
+ import { previewBridge } from '@b10cks/client'
254
+
255
+ // Call once on the client when your app boots (the framework packages do this for you).
256
+ previewBridge.init({ allowedOrigins: ['https://app.b10cks.com'] })
257
+
258
+ previewBridge.isInPreviewMode() // true only inside the editor iframe
259
+ previewBridge.selectItem(blockId) // tell the editor to select a block
260
+ previewBridge.selectField(blockId, path) // deep-select a nested field (e.g. rich text)
261
+ previewBridge.updateFieldAt(blockId, ['headline'], 'New text') // stream an inline edit
262
+
263
+ // Subscribe to editor → preview events; returns an unsubscribe function.
264
+ const off = previewBridge.on('CONTENT_UPDATE', ({ content }) => { /* … */ })
265
+ ```
266
+
267
+ Event protocol:
268
+
269
+ | Event | Direction | Payload | Purpose |
270
+ |---|---|---|---|
271
+ | `SELECT_UPDATE` | both | `{ selectedItem }` | Select a block |
272
+ | `HOVER_UPDATE` | editor → preview | `{ selectedItem }` | Hover-highlight a block |
273
+ | `FIELD_SELECT` | preview → editor | `{ itemId, path }` | Deep-select a field so the editor opens its own editor |
274
+ | `FIELD_UPDATE` | preview → editor | `{ itemId, path?, field?, value }` | Stream an inline edit |
275
+ | `CONTENT_UPDATE` | editor → preview | `{ content }` | Replace the whole content tree |
276
+ | `CONTENT_PATCH` | editor → preview | `{ path, value }` | Replace a single value at `path` |
277
+
278
+ `path` is a `FieldPath` (`(string | number)[]`) that addresses any value at any depth, including array indices — e.g. `['body', 2, 'headline']`.
279
+
280
+ #### Security
281
+
282
+ By default the bridge locks onto the origin of the first message it receives (trust-on-first-use) and ignores everything else afterwards; outbound messages are then targeted to that origin instead of `*`. Pass `allowedOrigins` to `init()` to restrict the handshake to a known list of editor origins.
283
+
284
+ ### Editable DOM helpers
285
+
286
+ `attachEditable` and `attachEditableField` wire a DOM element to the bridge and return a cleanup function. Both are no-ops outside preview mode.
287
+
288
+ ```typescript
289
+ import { attachEditable, attachEditableField } from '@b10cks/client'
290
+
291
+ // Selectable block: click selects it; editor-driven select/hover toggle outline classes.
292
+ const detach = attachEditable(el, { id: block.id })
293
+
294
+ // Inline string field (contenteditable, streams plain-text edits):
295
+ attachEditableField(el, { id: block.id, field: 'headline' })
296
+
297
+ // Rich text / complex field — deep-select instead of editing inline:
298
+ attachEditableField(el, { id: block.id, path: ['body'], mode: 'select' })
299
+ ```
300
+
301
+ ### Scroll offset (fixed headers)
302
+
303
+ When a block is selected it is scrolled into view with `block: 'nearest'` (so it never jumps when already visible) and honors the `--b10cks-scroll-offset` CSS variable as `scroll-margin-top`. Set it to your fixed header's height so selection doesn't overshoot beneath it — either in CSS:
304
+
305
+ ```css
306
+ :root { --b10cks-scroll-offset: 80px; }
307
+ ```
308
+
309
+ or from JS (the framework packages expose a `scrollOffset` option that calls this):
310
+
311
+ ```typescript
312
+ import { ensurePreviewStyles, setPreviewScrollOffset } from '@b10cks/client'
313
+
314
+ ensurePreviewStyles() // inject the outline + scroll-margin styles once
315
+ setPreviewScrollOffset(80) // number → px, or pass a string like '5rem'
316
+ ```
317
+
318
+ ### `PreviewStore` — reactive live content
319
+
320
+ `PreviewStore` is a framework-agnostic, subscribable holder for the content tree. `bindPreviewStore` feeds `CONTENT_UPDATE`/`CONTENT_PATCH` events into it, so any complex field — including rich text — re-renders from the new snapshot. The framework packages expose this as `usePreviewContent` / `createPreviewContent`.
321
+
322
+ ```typescript
323
+ import { PreviewStore, bindPreviewStore, setAtPath, getAtPath } from '@b10cks/client'
324
+
325
+ const store = new PreviewStore(initialContent)
326
+ const offBridge = bindPreviewStore(store)
327
+ const off = store.subscribe(() => render(store.getSnapshot()))
328
+
329
+ // Immutable path helpers used internally — also exported for your own use:
330
+ const next = setAtPath(content, ['body', 0, 'headline'], 'New')
331
+ const value = getAtPath(content, ['body', 0, 'headline'])
332
+ ```
333
+
244
334
  ## TypeScript
245
335
 
246
336
  Common types exported from the package root:
package/dist/index.d.ts CHANGED
@@ -5,6 +5,13 @@ export declare class ApiClient {
5
5
  private readonly fetchClient;
6
6
  private readonly getRvFn?;
7
7
  private readonly setRvFn?;
8
+ /**
9
+ * Per-instance revision store, used when no `getRv`/`setRv` callbacks are
10
+ * supplied. Kept on the instance (not module scope) so concurrent requests
11
+ * in a server runtime cannot leak revisions into one another. Server callers
12
+ * that need request-scoped persistence should pass `getRv`/`setRv`.
13
+ */
14
+ private rvValue;
8
15
  constructor(options: B10cksApiClientOptions, requestUrl?: URL | string);
9
16
  get<T>(endpoint: Endpoint, params?: Omit<IBBaseQueryParams, 'token'> & Record<string, unknown>): Promise<ApiResourceResponse<T>>;
10
17
  post<T>(endpoint: string, body?: unknown, params?: Omit<IBBaseQueryParams, 'token'> & Record<string, unknown>): Promise<T>;
@@ -24,6 +31,44 @@ declare type ApiQueryParams = Omit<IBBaseQueryParams, 'token'> & Record<string,
24
31
 
25
32
  declare type ApiResourceResponse<T> = IBResponse<T> | T;
26
33
 
34
+ /**
35
+ * Wire a DOM element as a selectable block: click selects it in the editor,
36
+ * and editor-driven select/hover toggle outline classes. Honors the configured
37
+ * scroll offset and only scrolls when the element isn't already in view.
38
+ * Returns a cleanup function. No-op outside preview mode.
39
+ */
40
+ export declare function attachEditable(el: HTMLElement, options: AttachEditableOptions): () => void;
41
+
42
+ /**
43
+ * Wire a DOM element for field editing. Returns a cleanup function. No-op
44
+ * outside preview mode. See {@link AttachEditableFieldOptions.mode}.
45
+ */
46
+ export declare function attachEditableField(el: HTMLElement, options: AttachEditableFieldOptions): () => void;
47
+
48
+ export declare interface AttachEditableFieldOptions {
49
+ id: string;
50
+ /** @deprecated Use `path`. Kept for flat string fields. */
51
+ field?: string;
52
+ path?: FieldPath;
53
+ /**
54
+ * `inline` makes the element contenteditable and streams plain-text edits
55
+ * back to the editor — suitable for simple string fields only. `select`
56
+ * instead deep-selects the field so the editor opens its own editor, which
57
+ * is the right choice for rich text and other complex types. Default
58
+ * `inline`, or `select` automatically when no `field` is given but a `path`
59
+ * targets a complex value.
60
+ */
61
+ mode?: EditableFieldMode;
62
+ }
63
+
64
+ export declare interface AttachEditableOptions {
65
+ id: string;
66
+ onSelectChange?: (selected: boolean) => void;
67
+ onHoverChange?: (hovered: boolean) => void;
68
+ /** Scroll the element into view when it becomes selected. Default true. */
69
+ scrollOnSelect?: boolean;
70
+ }
71
+
27
72
  export declare interface B10cksApiClientOptions {
28
73
  baseUrl: string;
29
74
  token: string;
@@ -96,6 +141,12 @@ export declare interface B10cksLinkResolved {
96
141
  target: string;
97
142
  }
98
143
 
144
+ /**
145
+ * Feed the singleton bridge's content events into a {@link PreviewStore}.
146
+ * Returns an unsubscribe function. No-op outside preview mode.
147
+ */
148
+ export declare function bindPreviewStore<T>(store: PreviewStore<T>): () => void;
149
+
99
150
  export declare type BridgeEvent = {
100
151
  type: EventType;
101
152
  payload: EventPayloadMap[EventType];
@@ -114,6 +165,12 @@ export declare interface CollectionFetchOptions {
114
165
  allPages?: boolean;
115
166
  }
116
167
 
168
+ /** Granular update: replace the value at `path` within the content tree. */
169
+ export declare type ContentPatchEvent = {
170
+ path: FieldPath;
171
+ value: unknown;
172
+ };
173
+
117
174
  export declare type ContentSortField = 'published_at' | 'updated_at' | 'created_at' | `content.${string}`;
118
175
 
119
176
  export declare type ContentSortItem = ContentSortField | `-${ContentSortField}`;
@@ -154,25 +211,52 @@ export declare type DateFilter = string | {
154
211
  '!null': true;
155
212
  };
156
213
 
214
+ export declare type EditableFieldMode = 'inline' | 'select';
215
+
157
216
  export declare type Endpoint = 'blocks' | `blocks/${string}` | 'contents' | `contents/${string}` | `datasources/${string}/entries` | 'datasources' | 'redirects' | 'search' | 'sitemap' | 'spaces/me';
158
217
 
218
+ /**
219
+ * Inject the preview outline styles once. Selected/hovered blocks get an
220
+ * outline; `.b10cks-preview` carries a `scroll-margin-top` so scroll-into-view
221
+ * clears a fixed app header. Set the offset via {@link setPreviewScrollOffset}
222
+ * or the `--b10cks-scroll-offset` CSS variable.
223
+ */
224
+ export declare function ensurePreviewStyles(): void;
225
+
159
226
  declare type EventCallback<T> = (payload: T) => void;
160
227
 
161
228
  export declare type EventPayloadMap = {
162
229
  CONTENT_UPDATE: ContentUpdateEvent;
230
+ CONTENT_PATCH: ContentPatchEvent;
163
231
  SELECT_UPDATE: SelectUpdateEvent;
164
232
  HOVER_UPDATE: SelectUpdateEvent;
165
233
  FIELD_UPDATE: FieldUpdateEvent;
234
+ FIELD_SELECT: FieldSelectEvent;
166
235
  };
167
236
 
168
- export declare type EventType = 'CONTENT_UPDATE' | 'SELECT_UPDATE' | 'HOVER_UPDATE' | 'FIELD_UPDATE';
237
+ export declare type EventType = 'CONTENT_UPDATE' | 'CONTENT_PATCH' | 'SELECT_UPDATE' | 'HOVER_UPDATE' | 'FIELD_UPDATE' | 'FIELD_SELECT';
169
238
 
170
239
  export declare type FetchClient = (input: URL | string | RequestInfo, init?: RequestInit) => Promise<unknown>;
171
240
 
241
+ /** Addresses a field within a block, supporting nested objects and arrays. */
242
+ export declare type FieldPath = (string | number)[];
243
+
244
+ /**
245
+ * Deep-select a specific field (e.g. a rich text field) so the editor opens
246
+ * its own editor for that field instead of editing it inline in the preview.
247
+ */
248
+ export declare type FieldSelectEvent = {
249
+ itemId: string;
250
+ path: FieldPath;
251
+ };
252
+
172
253
  export declare type FieldUpdateEvent = {
173
254
  itemId: string;
174
- field: string;
175
- value: string;
255
+ /** Path to the field within the block. Preferred over `field`. */
256
+ path?: FieldPath;
257
+ /** @deprecated Use `path`. Kept for flat string fields. */
258
+ field?: string;
259
+ value: unknown;
176
260
  };
177
261
 
178
262
  /**
@@ -180,6 +264,9 @@ export declare type FieldUpdateEvent = {
180
264
  */
181
265
  export declare function filterSitemapEntries(entries: IBSitemapEntry[], options?: SitemapFilterOptions): IBSitemapEntry[];
182
266
 
267
+ /** Read the value at `path` within `target`, or undefined if absent. */
268
+ export declare function getAtPath(target: unknown, path: FieldPath): unknown;
269
+
183
270
  export declare interface GetConfigOptions extends Omit<IBContentQueryParams, 'token' | 'full_slug'> {
184
271
  slug?: string;
185
272
  language?: string;
@@ -418,26 +505,63 @@ export declare type IdFilter = string | {
418
505
  '!null': true;
419
506
  };
420
507
 
508
+ declare type Listener = () => void;
509
+
421
510
  export declare function normalizePathSegment(value: string | null | undefined): string;
422
511
 
423
512
  export declare class PreviewBridge {
424
513
  private static instance;
425
514
  private readonly listeners;
426
515
  private initialized;
516
+ private allowedOrigins;
517
+ /** Origin of the editor, captured from the first trusted message. */
518
+ private parentOrigin;
427
519
  private constructor();
428
520
  static getInstance(): PreviewBridge;
429
- init(): void;
521
+ init(options?: PreviewBridgeInitOptions): void;
430
522
  destroy(): void;
431
523
  isInPreviewMode(): boolean;
432
524
  on<T extends EventType>(eventType: T, callback: EventCallback<EventPayloadMap[T]>): () => void;
433
525
  selectItem(selectedItem: string): void;
526
+ /** Deep-select a nested field (e.g. a rich text field) in the editor. */
527
+ selectField(itemId: string, path: FieldPath): void;
528
+ /** @deprecated Prefer {@link updateFieldAt} with a path. */
434
529
  updateField(itemId: string, field: string, value: string): void;
530
+ /** Stream an inline edit back to the editor, addressed by path. */
531
+ updateFieldAt(itemId: string, path: FieldPath, value: unknown): void;
532
+ private post;
435
533
  private handleMessage;
534
+ private isOriginTrusted;
436
535
  private notify;
437
536
  }
438
537
 
439
538
  export declare const previewBridge: PreviewBridge;
440
539
 
540
+ export declare interface PreviewBridgeInitOptions {
541
+ /**
542
+ * Origins the editor is allowed to post from. When provided, messages from
543
+ * any other origin are ignored. When omitted, the bridge locks onto the
544
+ * origin of the first valid message it receives (trust-on-first-use).
545
+ */
546
+ allowedOrigins?: string[];
547
+ }
548
+
549
+ /**
550
+ * A framework-agnostic, reactive holder for the content tree shown in the
551
+ * preview. The editor pushes whole-tree (`CONTENT_UPDATE`) or granular
552
+ * (`CONTENT_PATCH`) changes; subscribers re-render from the new snapshot.
553
+ */
554
+ export declare class PreviewStore<T = Record<string, unknown>> {
555
+ private content;
556
+ private readonly listeners;
557
+ constructor(initial: T);
558
+ subscribe: (listener: Listener) => (() => void);
559
+ getSnapshot: () => T;
560
+ setContent(next: T): void;
561
+ patch(path: FieldPath, value: unknown): void;
562
+ private emit;
563
+ }
564
+
441
565
  export declare interface RedirectFetchOptions extends CollectionFetchOptions {
442
566
  forceRefresh?: boolean;
443
567
  }
@@ -475,6 +599,16 @@ export declare type SelectUpdateEvent = {
475
599
 
476
600
  export declare function serializeFilter(filter: Record<string, unknown>): Record<string, string>;
477
601
 
602
+ /** Return a copy of `target` with the value at `path` replaced. Immutable. */
603
+ export declare function setAtPath<T>(target: T, path: FieldPath, value: unknown): T;
604
+
605
+ /**
606
+ * Set the scroll offset used when a selected block is scrolled into view.
607
+ * Pass your fixed header's height so selection no longer overshoots beneath it.
608
+ * A number is treated as pixels; strings are used verbatim (e.g. `"5rem"`).
609
+ */
610
+ export declare function setPreviewScrollOffset(offset: number | string): void;
611
+
478
612
  export declare interface SitemapFilterOptions {
479
613
  /** Absolute base URL used for deduplication. Without it, paths are compared as strings. */
480
614
  siteUrl?: string;
package/dist/index.js CHANGED
@@ -1,8 +1,19 @@
1
- "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function C(r){if(!r)return;if(r.type==="email"){const n=new URLSearchParams;r.subject&&n.set("subject",r.subject),r.body&&n.set("body",r.body),r.cc&&n.set("cc",r.cc),r.bcc&&n.set("bcc",r.bcc);const i=n.toString();return{href:`mailto:${r.email}${i?`?${i}`:""}`,target:"_self"}}if(r.type==="asset")return;const t=r.target??"_self";let e=r.url??"";return"anchor"in r&&r.anchor&&(e=`${e.trimEnd()}#${r.anchor}`),{href:e,target:t}}function j(r){if(r!=null){if(typeof r=="boolean"||typeof r=="number")return String(r);if(typeof r=="string")return r;if(typeof r=="object"&&!Array.isArray(r)){const t=r;if("null"in t)return"null:";if("!null"in t)return"!null:";if("between"in t){const[n,i]=t.between;return`${n}...${i}`}const e=["eq","neq","like","!like","^like","like$","lt","gt","lte","gte","in","!in","empty","!empty"];for(const n of e)if(n in t){const i=t[n];return Array.isArray(i)?`${n}:${i.join(",")}`:`${n}:${String(i)}`}}}}function y(r){const t={};for(const[e,n]of Object.entries(r)){const i=j(n);i!==void 0&&(t[e]=i)}return t}function f(r,t,e){const n=t&&typeof t=="object"?y(t):{},i=Array.isArray(e)?{sort:e.join(",")}:e!=null?{sort:e}:{};return{...r,...n,...i}}class b{constructor(t){this.client=t}redirectsCache=null;configCache=new Map;async getResource(t,e={}){const n=await this.client.get(t,e);return this.unwrapResource(n)}async getCollection(t,e={},n={}){if(n.allPages)return this.client.getAll(t,e);const i=await this.client.get(t,e);return this.unwrapCollection(i)}async getContent(t,e={}){return this.getResource(`contents/${t}`,e)}async getContents(t={},e={}){const{filter:n,sort:i,...s}=t;return this.getCollection("contents",f(s,n,i),e)}async getBlock(t,e={}){return this.getResource(`blocks/${t}`,e)}async getBlocks(t={},e={}){const{filter:n,...i}=t;return this.getCollection("blocks",f(i,n,void 0),e)}async search(t){const e=await this.client.get("search",t);if(typeof e=="object"&&e!==null&&"data"in e&&"meta"in e)return e;if(typeof e=="object"&&e!==null&&"data"in e){const n=e.data;if(typeof n=="object"&&n!==null&&"meta"in n&&"data"in n)return n}return e}async getSitemap(t={},e={}){return this.getCollection("sitemap",t,e)}async getDataEntries(t,e={},n={}){return this.getCollection(`datasources/${t}/entries`,e,n)}async getDataSources(t={},e={}){return this.getCollection("datasources",t,e)}async getSpace(t={}){return this.getResource("spaces/me",t)}async lookupRedirect(t){if(!this.client.post)throw new Error("lookupRedirect requires a client that supports POST. Use ApiClient from @b10cks/client.");return this.client.post("redirects/lookup",{source:t})}async getRedirects(t={},e=!1){const{allPages:n=!1,forceRefresh:i=!1}=typeof e=="boolean"?{allPages:!0,forceRefresh:e}:e;if(n&&this.redirectsCache&&!i)return this.redirectsCache;const{filter:s,...a}=t,o=f(a,s,void 0),u=await this.getCollection("redirects",o,{allPages:n}),l=Object.fromEntries(u.map(({source:w,target:v,status_code:R})=>[w,{target:v,status_code:R}]));return n&&(this.redirectsCache=l),l}async getConfig({slug:t="_config",language_iso:e,language:n,bypassCache:i=!1,...s}={}){const a=e??n,o=JSON.stringify({slug:t,language_iso:a,...s});if(!i&&this.configCache.has(o))return this.configCache.get(o);const l=(await this.getContent(t,{...s,language_iso:a})).content??{};return i||this.configCache.set(o,l),l}async syncRevision(t=426713400){const n=(await this.getSpace()).rv||t;return this.client.setRv(n),n}clearCache(){this.redirectsCache=null,this.configCache.clear()}unwrapResource(t){return typeof t=="object"&&t!==null&&"data"in t?t.data:t}unwrapCollection(t){if(Array.isArray(t))return t;if(typeof t=="object"&&t!==null&&"data"in t){if(Array.isArray(t.data))return t.data;if(typeof t.data=="object"&&t.data!==null&&"data"in t.data&&Array.isArray(t.data.data))return t.data.data}return[]}}function S(r){return new b(r)}class c{static instance;listeners={};initialized=!1;constructor(){}static getInstance(){return c.instance||(c.instance=new c),c.instance}init(){!this.isInPreviewMode()||this.initialized||(window.addEventListener("message",this.handleMessage),this.initialized=!0)}destroy(){this.initialized&&typeof window<"u"&&window.removeEventListener("message",this.handleMessage),this.initialized=!1;for(const t of Object.keys(this.listeners))this.listeners[t]?.clear()}isInPreviewMode(){return typeof window<"u"&&window.self!==window.top}on(t,e){if(!this.isInPreviewMode())return()=>{};const n=this.listeners[t]||new Set;return this.listeners[t]=n,n.add(e),()=>{n.delete(e)}}selectItem(t){this.isInPreviewMode()&&window.parent.postMessage({type:"SELECT_UPDATE",payload:{selectedItem:t}},"*")}updateField(t,e,n){this.isInPreviewMode()&&window.parent.postMessage({type:"FIELD_UPDATE",payload:{itemId:t,field:e,value:n}},"*")}handleMessage=t=>{if(!t.data||typeof t.data!="object")return;const{type:e,payload:n}=t.data;!e||!n||this.notify(e,n)};notify(t,e){const n=this.listeners[t];if(n)for(const i of n)i(e)}}const A=c.getInstance();function d(r){return(r??"").trim().replace(/^\/+|\/+$/g,"")}function p(r,t){const e=d(r),n=d(t);if(!n)return e&&e!=="home"?`/${e}`:"/";if(!e||e==="home")return`/${n}`;const i=`${n}/`;return e===n||e.startsWith(i)?`/${e}`:`/${n}/${e}`}function m(r,t){if(!t)return null;try{return new URL(r,t).toString()}catch{return null}}function g(r){return r.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")}function $(r,t={}){const{siteUrl:e,locale:n}=t,i=new Set;return r.filter(s=>{if(n&&s.language_iso!==n||s.meta?.robots?.toLowerCase().includes("noindex"))return!1;const a=p(s.full_slug,s.language_iso),o=e?m(a,e)??a:a;return i.has(o)?!1:(i.add(o),!0)})}function P(r,t){return['<?xml version="1.0" encoding="UTF-8"?>','<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',r.map(n=>{const i=p(n.full_slug,n.language_iso),s=t?m(i,t):i;if(!s)return"";const a=[" <url>",` <loc>${g(s)}</loc>`];return n.published_at&&a.push(` <lastmod>${g(n.published_at)}</lastmod>`),a.push(" </url>"),a.join(`
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});function P(i){if(!i)return;if(i.type==="email"){const n=new URLSearchParams;i.subject&&n.set("subject",i.subject),i.body&&n.set("body",i.body),i.cc&&n.set("cc",i.cc),i.bcc&&n.set("bcc",i.bcc);const s=n.toString();return{href:`mailto:${i.email}${s?`?${s}`:""}`,target:"_self"}}if(i.type==="asset")return;const t=i.target??"_self";let e=i.url??"";return"anchor"in i&&i.anchor&&(e=`${e.trimEnd()}#${i.anchor}`),{href:e,target:t}}function T(i){if(i!=null){if(typeof i=="boolean"||typeof i=="number")return String(i);if(typeof i=="string")return i;if(typeof i=="object"&&!Array.isArray(i)){const t=i;if("null"in t)return"null:";if("!null"in t)return"!null:";if("between"in t){const[n,s]=t.between;return`${n}...${s}`}const e=["eq","neq","like","!like","^like","like$","lt","gt","lte","gte","in","!in","empty","!empty"];for(const n of e)if(n in t){const s=t[n];return Array.isArray(s)?`${n}:${s.join(",")}`:`${n}:${String(s)}`}}}}function R(i){const t={};for(const[e,n]of Object.entries(i)){const s=T(n);s!==void 0&&(t[e]=s)}return t}function m(i,t,e){const n=t&&typeof t=="object"?R(t):{},s=Array.isArray(e)?{sort:e.join(",")}:e!=null?{sort:e}:{};return{...i,...n,...s}}class A{constructor(t){this.client=t}redirectsCache=null;configCache=new Map;async getResource(t,e={}){const n=await this.client.get(t,e);return this.unwrapResource(n)}async getCollection(t,e={},n={}){if(n.allPages)return this.client.getAll(t,e);const s=await this.client.get(t,e);return this.unwrapCollection(s)}async getContent(t,e={}){return this.getResource(`contents/${t}`,e)}async getContents(t={},e={}){const{filter:n,sort:s,...r}=t;return this.getCollection("contents",m(r,n,s),e)}async getBlock(t,e={}){return this.getResource(`blocks/${t}`,e)}async getBlocks(t={},e={}){const{filter:n,...s}=t;return this.getCollection("blocks",m(s,n,void 0),e)}async search(t){const e=await this.client.get("search",t);if(typeof e=="object"&&e!==null&&"data"in e&&"meta"in e)return e;if(typeof e=="object"&&e!==null&&"data"in e){const n=e.data;if(typeof n=="object"&&n!==null&&"meta"in n&&"data"in n)return n}return e}async getSitemap(t={},e={}){return this.getCollection("sitemap",t,e)}async getDataEntries(t,e={},n={}){return this.getCollection(`datasources/${t}/entries`,e,n)}async getDataSources(t={},e={}){return this.getCollection("datasources",t,e)}async getSpace(t={}){return this.getResource("spaces/me",t)}async lookupRedirect(t){if(!this.client.post)throw new Error("lookupRedirect requires a client that supports POST. Use ApiClient from @b10cks/client.");return this.client.post("redirects/lookup",{source:t})}async getRedirects(t={},e=!1){const{allPages:n=!1,forceRefresh:s=!1}=typeof e=="boolean"?{allPages:!0,forceRefresh:e}:e;if(n&&this.redirectsCache&&!s)return this.redirectsCache;const{filter:r,...o}=t,a=m(o,r,void 0),c=await this.getCollection("redirects",a,{allPages:n}),d=Object.fromEntries(c.map(({source:g,target:u,status_code:f})=>[g,{target:u,status_code:f}]));return n&&(this.redirectsCache=d),d}async getConfig({slug:t="_config",language_iso:e,language:n,bypassCache:s=!1,...r}={}){const o=e??n,a=JSON.stringify({slug:t,language_iso:o,...r});if(!s&&this.configCache.has(a))return this.configCache.get(a);const d=(await this.getContent(t,{...r,language_iso:o})).content??{};return s||this.configCache.set(a,d),d}async syncRevision(t=426713400){const n=(await this.getSpace()).rv||t;return this.client.setRv(n),n}clearCache(){this.redirectsCache=null,this.configCache.clear()}unwrapResource(t){return typeof t=="object"&&t!==null&&"data"in t?t.data:t}unwrapCollection(t){if(Array.isArray(t))return t;if(typeof t=="object"&&t!==null&&"data"in t){if(Array.isArray(t.data))return t.data;if(typeof t.data=="object"&&t.data!==null&&"data"in t.data&&Array.isArray(t.data.data))return t.data.data}return[]}}function k(i){return new A(i)}const j=new Set(["CONTENT_UPDATE","CONTENT_PATCH","SELECT_UPDATE","HOVER_UPDATE","FIELD_UPDATE","FIELD_SELECT"]),L="B10CKS_BRIDGE_READY";class h{static instance;listeners={};initialized=!1;allowedOrigins=null;parentOrigin=null;constructor(){}static getInstance(){return h.instance||(h.instance=new h),h.instance}init(t={}){!this.isInPreviewMode()||this.initialized||(this.allowedOrigins=t.allowedOrigins??null,window.addEventListener("message",this.handleMessage),this.initialized=!0,window.parent.postMessage({type:L},"*"))}destroy(){this.initialized&&typeof window<"u"&&window.removeEventListener("message",this.handleMessage),this.initialized=!1,this.parentOrigin=null;for(const t of Object.keys(this.listeners))this.listeners[t]?.clear()}isInPreviewMode(){return typeof window<"u"&&window.self!==window.top}on(t,e){if(!this.isInPreviewMode())return()=>{};const n=this.listeners[t]||new Set;return this.listeners[t]=n,n.add(e),()=>{n.delete(e)}}selectItem(t){this.post("SELECT_UPDATE",{selectedItem:t})}selectField(t,e){this.post("FIELD_SELECT",{itemId:t,path:e})}updateField(t,e,n){this.post("FIELD_UPDATE",{itemId:t,field:e,value:n})}updateFieldAt(t,e,n){this.post("FIELD_UPDATE",{itemId:t,path:e,value:n})}post(t,e){this.isInPreviewMode()&&window.parent.postMessage({type:t,payload:e},this.parentOrigin??"*")}handleMessage=t=>{if(!this.isOriginTrusted(t.origin)||!t.data||typeof t.data!="object")return;const{type:e,payload:n}=t.data;!e||!j.has(e)||!n||(!this.parentOrigin&&t.origin&&t.origin!=="null"&&(this.parentOrigin=t.origin),this.notify(e,n))};isOriginTrusted(t){return this.parentOrigin?t===this.parentOrigin:this.allowedOrigins?this.allowedOrigins.includes(t):!0}notify(t,e){const n=this.listeners[t];if(n)for(const s of n)s(e)}}const l=h.getInstance(),C="b10cks-preview-style",S="--b10cks-scroll-offset";function v(){if(typeof document>"u"||document.getElementById(C))return;const i=document.createElement("style");i.id=C,i.textContent=`
2
+ .b10cks-preview { scroll-margin-top: var(${S}, 0px); }
3
+ .b10cks-hover,
4
+ .b10cks-preview:hover {
5
+ outline: 2px dashed rgba(59, 130, 246, 0.5);
6
+ outline-offset: 2px;
7
+ }
8
+ .b10cks-selected {
9
+ outline: 2px solid rgb(59, 130, 246) !important;
10
+ outline-offset: 2px;
11
+ }
12
+ `,document.head.appendChild(i)}function _(i){if(typeof document>"u")return;const t=typeof i=="number"?`${i}px`:i;document.documentElement.style.setProperty(S,t)}function F(i,t){if(!l.isInPreviewMode()||!t.id)return()=>{};v();const{id:e,onSelectChange:n,onHoverChange:s,scrollOnSelect:r=!0}=t;i.classList.add("b10cks-preview");const o=u=>{u.preventDefault(),u.stopPropagation(),l.selectItem(e)},a=({selectedItem:u})=>{const f=u===e;i.classList.toggle("b10cks-selected",f),f&&r&&$(i),n?.(f)},c=({selectedItem:u})=>{const f=u===e;i.classList.toggle("b10cks-hover",f),s?.(f)};i.addEventListener("click",o);const d=l.on("SELECT_UPDATE",a),g=l.on("HOVER_UPDATE",c);return()=>{i.removeEventListener("click",o),d(),g(),i.classList.remove("b10cks-preview","b10cks-selected","b10cks-hover")}}function O(i,t){if(!l.isInPreviewMode()||!t.id)return()=>{};const{id:e,field:n,path:s,mode:r="inline"}=t;if(r==="select"){v(),i.classList.add("b10cks-preview");const a=c=>{c.preventDefault(),c.stopPropagation(),s?l.selectField(e,s):l.selectItem(e)};return i.addEventListener("click",a),()=>{i.removeEventListener("click",a),i.classList.remove("b10cks-preview")}}i.setAttribute("contenteditable","true");const o=a=>{const c=a.target.innerText;s?l.updateFieldAt(e,s,c):l.updateField(e,n??"",c)};return i.addEventListener("input",o),()=>{i.removeEventListener("input",o),i.removeAttribute("contenteditable")}}function $(i){typeof i.scrollIntoView=="function"&&i.scrollIntoView({block:"nearest",behavior:"smooth"})}function D(i,t){let e=i;for(const n of t){if(e==null||typeof e!="object")return;e=e[n]}return e}function p(i,t,e){if(t.length===0)return e;const[n,...s]=t;if(n===void 0)return e;if(typeof n=="number"){const o=Array.isArray(i)?i.slice():[];return o[n]=p(o[n],s,e),o}const r=i&&typeof i=="object"&&!Array.isArray(i)?{...i}:{};return r[n]=p(r[n],s,e),r}class I{content;listeners=new Set;constructor(t){this.content=t}subscribe=t=>(this.listeners.add(t),()=>{this.listeners.delete(t)});getSnapshot=()=>this.content;setContent(t){this.content=t,this.emit()}patch(t,e){this.content=p(this.content,t,e),this.emit()}emit(){for(const t of this.listeners)t()}}function U(i){const t=l.on("CONTENT_UPDATE",({content:n})=>{i.setContent(n)}),e=l.on("CONTENT_PATCH",({path:n,value:s})=>{i.patch(n,s)});return()=>{t(),e()}}function y(i){return(i??"").trim().replace(/^\/+|\/+$/g,"")}function w(i,t){const e=y(i),n=y(t);if(!n)return e&&e!=="home"?`/${e}`:"/";if(!e||e==="home")return`/${n}`;const s=`${n}/`;return e===n||e.startsWith(s)?`/${e}`:`/${n}/${e}`}function E(i,t){if(!t)return null;try{return new URL(i,t).toString()}catch{return null}}function b(i){return i.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&apos;")}function x(i,t={}){const{siteUrl:e,locale:n}=t,s=new Set;return i.filter(r=>{if(n&&r.language_iso!==n||r.meta?.robots?.toLowerCase().includes("noindex"))return!1;const o=w(r.full_slug,r.language_iso),a=e?E(o,e)??o:o;return s.has(a)?!1:(s.add(a),!0)})}function z(i,t){return['<?xml version="1.0" encoding="UTF-8"?>','<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i.map(n=>{const s=w(n.full_slug,n.language_iso),r=t?E(s,t):s;if(!r)return"";const o=[" <url>",` <loc>${b(r)}</loc>`];return n.published_at&&o.push(` <lastmod>${b(n.published_at)}</lastmod>`),o.push(" </url>"),o.join(`
2
13
  `)}).filter(Boolean).join(`
3
14
  `),"</urlset>"].join(`
4
- `)}function F(r,t){return['<?xml version="1.0" encoding="UTF-8"?>','<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',r.map(n=>{const i=t?m(n,t):n;return i?[" <sitemap>",` <loc>${g(i)}</loc>`," </sitemap>"].join(`
15
+ `)}function M(i,t){return['<?xml version="1.0" encoding="UTF-8"?>','<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i.map(n=>{const s=t?E(n,t):n;return s?[" <sitemap>",` <loc>${b(s)}</loc>`," </sitemap>"].join(`
5
16
  `):""}).filter(Boolean).join(`
6
17
  `),"</sitemapindex>"].join(`
7
- `)}const z=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"}));let h=0;class k{baseUrl;token;vid;fetchClient;getRvFn;setRvFn;constructor(t,e){this.baseUrl=t.baseUrl.endsWith("/")?t.baseUrl.slice(0,-1):t.baseUrl,this.token=t.token;const n=typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):null;if(t.fetchClient)this.fetchClient=t.fetchClient;else if(n)this.fetchClient=n;else throw new Error("No fetch implementation available. Provide `fetchClient` in ApiClient options.");this.getRvFn=t.getRv,this.setRvFn=t.setRv;const i=this.resolveRequestUrl(e);this.vid=t.version||"published",this.setRv(i?.searchParams.get("b10cks_rv")||t.rv||h)}async get(t,e={}){const n=this.buildUrl(t,{vid:this.vid,rv:this.getRv(),...e,token:this.token}),i=await this.fetchClient(n),s=await this.parseResponse(i);return this.hasRevision(s)&&this.setRv(s.rv),s}async post(t,e,n={}){const i=this.buildUrl(t,{vid:this.vid,rv:this.getRv(),...n,token:this.token}),s=await this.fetchClient(i,{method:"POST",headers:{"Content-Type":"application/json"},body:e!==void 0?JSON.stringify(e):void 0});return this.parseResponse(s)}async getAll(t,e={}){const n=await this.get(t,{...e,page:1}),i=this.normalizeCollectionResponse(n);if(i.rv&&this.setRv(i.rv),!i.meta||i.meta.last_page<=1)return i.data;const s=Array.from({length:i.meta.last_page-1},(o,u)=>this.get(t,{...e,page:u+2})),a=await Promise.all(s);return i.data.concat(a.flatMap(o=>this.normalizeCollectionResponse(o).data))}setRv(t){this.setRvFn?this.setRvFn(t):h=t}getRv(){return this.getRvFn?this.getRvFn():h}async parseResponse(t){if(this.isFetchResponse(t)){if(!t.ok)throw new Error(`Request failed with status ${t.status}`);return await t.json()}return t}normalizeCollectionResponse(t){if(this.isCollectionResponseEnvelope(t)){const e=t.data;return{data:e.data,meta:e.meta,rv:t.rv||e.rv}}if(Array.isArray(t))return{data:t,meta:void 0,rv:void 0};if(typeof t=="object"&&t!==null&&"data"in t){const{data:e,meta:n,rv:i}=t;return{data:e,meta:n,rv:i}}return{data:[],meta:void 0,rv:void 0}}buildUrl(t,e){const n=new URL(`${this.baseUrl}/v1/${t}`);return Object.entries(e).forEach(([i,s])=>{s!=null&&n.searchParams.append(i,String(s))}),n.toString()}resolveRequestUrl(t){return t?t instanceof URL?t:new URL(t,"http://localhost"):null}isFetchResponse(t){return typeof t=="object"&&t!==null&&"json"in t&&typeof t.json=="function"&&"ok"in t}hasRevision(t){return typeof t=="object"&&t!==null&&"rv"in t}isCollectionResponseEnvelope(t){return typeof t=="object"&&t!==null&&"data"in t&&typeof t.data=="object"&&t.data!==null&&"data"in t.data&&Array.isArray(t.data.data)}}exports.ApiClient=k;exports.B10cksDataApi=b;exports.PreviewBridge=c;exports.buildLocalizedPath=p;exports.createB10cksDataApi=S;exports.filterSitemapEntries=$;exports.normalizePathSegment=d;exports.previewBridge=A;exports.renderSitemapIndex=F;exports.renderSitemapXml=P;exports.resolveB10cksLink=C;exports.serializeFilter=y;exports.types=z;
18
+ `)}const B=Object.freeze(Object.defineProperty({__proto__:null},Symbol.toStringTag,{value:"Module"}));class N{baseUrl;token;vid;fetchClient;getRvFn;setRvFn;rvValue=0;constructor(t,e){this.baseUrl=t.baseUrl.endsWith("/")?t.baseUrl.slice(0,-1):t.baseUrl,this.token=t.token;const n=typeof globalThis.fetch=="function"?globalThis.fetch.bind(globalThis):null;if(t.fetchClient)this.fetchClient=t.fetchClient;else if(n)this.fetchClient=n;else throw new Error("No fetch implementation available. Provide `fetchClient` in ApiClient options.");this.getRvFn=t.getRv,this.setRvFn=t.setRv;const s=this.resolveRequestUrl(e);this.vid=t.version||"published",this.setRv(s?.searchParams.get("b10cks_rv")||t.rv||0)}async get(t,e={}){const n=this.buildUrl(t,{vid:this.vid,rv:this.getRv(),...e,token:this.token}),s=await this.fetchClient(n),r=await this.parseResponse(s);return this.hasRevision(r)&&this.setRv(r.rv),r}async post(t,e,n={}){const s=this.buildUrl(t,{vid:this.vid,rv:this.getRv(),...n,token:this.token}),r=await this.fetchClient(s,{method:"POST",headers:{"Content-Type":"application/json"},body:e!==void 0?JSON.stringify(e):void 0});return this.parseResponse(r)}async getAll(t,e={}){const n=await this.get(t,{...e,page:1}),s=this.normalizeCollectionResponse(n);if(s.rv&&this.setRv(s.rv),!s.meta||s.meta.last_page<=1)return s.data;const r=Array.from({length:s.meta.last_page-1},(a,c)=>this.get(t,{...e,page:c+2})),o=await Promise.all(r);return s.data.concat(o.flatMap(a=>this.normalizeCollectionResponse(a).data))}setRv(t){this.setRvFn?this.setRvFn(t):this.rvValue=t}getRv(){return this.getRvFn?this.getRvFn():this.rvValue}async parseResponse(t){if(this.isFetchResponse(t)){if(!t.ok)throw new Error(`Request failed with status ${t.status}`);return await t.json()}return t}normalizeCollectionResponse(t){if(this.isCollectionResponseEnvelope(t)){const e=t.data;return{data:e.data,meta:e.meta,rv:t.rv||e.rv}}if(Array.isArray(t))return{data:t,meta:void 0,rv:void 0};if(typeof t=="object"&&t!==null&&"data"in t){const{data:e,meta:n,rv:s}=t;return{data:e,meta:n,rv:s}}return{data:[],meta:void 0,rv:void 0}}buildUrl(t,e){const n=new URL(`${this.baseUrl}/v1/${t}`);return Object.entries(e).forEach(([s,r])=>{r!=null&&n.searchParams.append(s,String(r))}),n.toString()}resolveRequestUrl(t){return t?t instanceof URL?t:new URL(t,"http://localhost"):null}isFetchResponse(t){return typeof t=="object"&&t!==null&&"json"in t&&typeof t.json=="function"&&"ok"in t}hasRevision(t){return typeof t=="object"&&t!==null&&"rv"in t}isCollectionResponseEnvelope(t){return typeof t=="object"&&t!==null&&"data"in t&&typeof t.data=="object"&&t.data!==null&&"data"in t.data&&Array.isArray(t.data.data)}}exports.ApiClient=N;exports.B10cksDataApi=A;exports.PreviewBridge=h;exports.PreviewStore=I;exports.attachEditable=F;exports.attachEditableField=O;exports.bindPreviewStore=U;exports.buildLocalizedPath=w;exports.createB10cksDataApi=k;exports.ensurePreviewStyles=v;exports.filterSitemapEntries=x;exports.getAtPath=D;exports.normalizePathSegment=y;exports.previewBridge=l;exports.renderSitemapIndex=M;exports.renderSitemapXml=z;exports.resolveB10cksLink=P;exports.serializeFilter=R;exports.setAtPath=p;exports.setPreviewScrollOffset=_;exports.types=B;
8
19
  //# sourceMappingURL=index.js.map