efficient-storefront-blocks 0.0.0 → 0.11.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.
Files changed (36) hide show
  1. package/CHANGELOG.md +397 -0
  2. package/LICENSE +10 -0
  3. package/README.md +279 -2
  4. package/dist/chunks/EditorPreviewCheckout-J27HMNBW.js +4 -0
  5. package/dist/chunks/PreviewFooter-BMBCVZQC.js +1 -0
  6. package/dist/chunks/SectionEditorShell-K4OEOAY4.js +1 -0
  7. package/dist/chunks/SiteEditorProvider-ECGZVKLG.js +1 -0
  8. package/dist/chunks/StorefrontEditorChrome-T6DTL7T7.js +1 -0
  9. package/dist/chunks/chunk-3J4FEDNZ.js +2 -0
  10. package/dist/chunks/chunk-6CP4D6VJ.js +1 -0
  11. package/dist/chunks/chunk-6XRKNA2E.js +1 -0
  12. package/dist/chunks/chunk-C2CHF6AL.js +15 -0
  13. package/dist/chunks/chunk-C2CHF6AL.js.LEGAL.txt +27 -0
  14. package/dist/chunks/chunk-ENDWECYV.js +1 -0
  15. package/dist/chunks/chunk-HVOO2GNE.js +1 -0
  16. package/dist/chunks/chunk-MYSIADXX.js +1 -0
  17. package/dist/chunks/chunk-NJXX5BI2.js +1 -0
  18. package/dist/chunks/chunk-RRHBLOH4.js +1 -0
  19. package/dist/chunks/chunk-TGIEMCNA.js +1 -0
  20. package/dist/chunks/chunk-THATLL5Z.js +230 -0
  21. package/dist/chunks/chunk-TOY67YUY.js +153 -0
  22. package/dist/chunks/chunk-TOY67YUY.js.LEGAL.txt +4 -0
  23. package/dist/chunks/chunk-UBWM4USZ.js +1 -0
  24. package/dist/chunks/chunk-Y6SLVHK3.js +1 -0
  25. package/dist/chunks/fitment-data-BXI7KQRZ.js +1 -0
  26. package/dist/chunks/gallery-builds-data-SWA6XKFF.js +1 -0
  27. package/dist/chunks/headers-VF4VTX37.js +1 -0
  28. package/dist/chunks/render-client-UQ5CUPRL.js +1 -0
  29. package/dist/contract.d.ts +48 -0
  30. package/dist/contract.js +1 -0
  31. package/dist/editor.css +1 -0
  32. package/dist/index.d.ts +721 -0
  33. package/dist/index.js +11361 -0
  34. package/dist/links.d.ts +86 -0
  35. package/dist/links.js +1 -0
  36. package/package.json +54 -3
@@ -0,0 +1,721 @@
1
+ /**
2
+ * The public types of `efficient-storefront-blocks` (published as dist/index.d.ts).
3
+ *
4
+ * Written out rather than generated: the entry reaches ~700 shop modules and a
5
+ * declaration bundle of them would publish the shop's internals as API. Kept
6
+ * honest by `types.check.ts` beside it, which the shop's own `tsc` compiles —
7
+ * each type here must be mutually assignable with the one the code defines.
8
+ */
9
+ import type { ComponentType, ReactElement } from "react";
10
+ // The engine's own block type: a host's blocks are written exactly as the
11
+ // editor's are. `efficient-easel` is a peer of this package.
12
+ import type { ComponentConfig } from "efficient-easel/blocks";
13
+
14
+ /** What the page editor is pointed at: a website page (site + slug) or a page
15
+ * of a website template. */
16
+ export type EditorTarget =
17
+ | { kind: "page"; slug: string; siteId?: string; originSlug?: string }
18
+ | { kind: "template"; id: string; pageSlug?: string };
19
+
20
+ /** The standalone one-section editor's target (a section template). */
21
+ export type SectionEditorTarget = { kind: "section"; id: string; type?: string; name?: string };
22
+
23
+ /** A media item the host's picker returns. */
24
+ export interface HostPickedMedia {
25
+ id: string;
26
+ url: string;
27
+ kind: "image" | "video";
28
+ }
29
+
30
+ /** What a field asked the media panel for. `file` is a file dropped straight on
31
+ * the field: the host uploads it and picks it without waiting for a click. */
32
+ export interface MediaPickRequest {
33
+ requestId: string;
34
+ /** `image/*`, `video/*` or `image/svg+xml` — what the field can hold. */
35
+ accept: string;
36
+ /** The field's label, e.g. "Background image". */
37
+ title: string;
38
+ file?: File | null;
39
+ }
40
+
41
+ /** The props the host's media panel is drawn with. */
42
+ export interface MediaPanelProps {
43
+ /** The field choosing right now, or null while the merchant is browsing. */
44
+ pick: MediaPickRequest | null;
45
+ /** Answer the field. Ignored while browsing. */
46
+ onPick: (media: HostPickedMedia) => void;
47
+ }
48
+
49
+ /** The host's media library, as a component the editor draws in its left
50
+ * panel (the rail's Media view, and every image field's pick mode). */
51
+ export type MediaPanelComponent = ComponentType<MediaPanelProps>;
52
+
53
+ /** Every message the editor sends its host. */
54
+ export type EditorToHostMessage =
55
+ | { type: "storefront-editor:ready" }
56
+ | { type: "storefront-editor:auth-expired" }
57
+ | { type: "storefront-editor:dirty" }
58
+ | { type: "storefront-editor:saved" }
59
+ | { type: "storefront-editor:published" }
60
+ | { type: "storefront-editor:save-failed" }
61
+ | { type: "storefront-editor:navigated"; target: EditorTarget }
62
+ | {
63
+ type: "storefront-editor:open-media-picker";
64
+ requestId: string;
65
+ accept: string;
66
+ title: string;
67
+ file?: File;
68
+ }
69
+ /** `mode: "create"` asks for a new form and carries a `requestId` to answer
70
+ * with `form-saved` / `form-closed`. `mode: "edit"` opens the form builder on
71
+ * `formId` and carries none: nothing waits on it. */
72
+ | { type: "storefront-editor:open-form"; requestId?: string; mode: "create" | "edit"; formId?: string }
73
+ | { type: "storefront-editor:open-category"; categoryId: string; name?: string }
74
+ | { type: "storefront-editor:open-product"; slug: string; name?: string }
75
+ | { type: "storefront-editor:open-post"; slug: string; title?: string }
76
+ | { type: "storefront-editor:open-menus"; menuId?: string | null }
77
+ | { type: "storefront-editor:open-page"; slug: string }
78
+ | { type: "storefront-editor:new-page" }
79
+ | { type: "storefront-editor:save-as-template"; siteId: string; slug: string };
80
+
81
+ /** Every message a host sends the editor. */
82
+ export type HostToEditorMessage =
83
+ | {
84
+ type: "storefront-editor:auth";
85
+ token: string;
86
+ siteId?: string;
87
+ brandId?: string;
88
+ theme?: string;
89
+ can?: { canCreatePlatformTemplate?: boolean };
90
+ }
91
+ | { type: "storefront-editor:navigate"; target: EditorTarget | SectionEditorTarget }
92
+ | { type: "storefront-editor:category-updated"; categoryId?: string }
93
+ | { type: "storefront-editor:menus-updated" }
94
+ | { type: "storefront-editor:form-saved"; requestId?: string; form?: { id: string; slug: string } }
95
+ | { type: "storefront-editor:form-closed"; requestId?: string }
96
+ | { type: "storefront-editor:media-picked"; requestId: string; media: HostPickedMedia }
97
+ | { type: "storefront-editor:media-pick-cancelled"; requestId: string };
98
+
99
+ export type EditorMessageType = EditorToHostMessage["type"];
100
+ type PayloadOf<T extends EditorMessageType> = Omit<Extract<EditorToHostMessage, { type: T }>, "type">;
101
+
102
+ export type SendToHost = <T extends EditorMessageType>(
103
+ type: T,
104
+ ...payload: Record<string, never> extends PayloadOf<T> ? [PayloadOf<T>?] : [PayloadOf<T>]
105
+ ) => void;
106
+
107
+ export type InboundHostMessage = { type: string } & Record<string, unknown>;
108
+
109
+ /** The editor's address — for an embedder, retargets of its own window. */
110
+ export interface EditorHostAddress {
111
+ /** The query the editor was opened with. An embedder that opens templates to
112
+ * edit their layout answers `"?editTemplate=1"`. */
113
+ search(): string;
114
+ /** Keep the embedder's notion of "where" in step (no load). */
115
+ show(target: EditorTarget | SectionEditorTarget): void;
116
+ /** Leave for another document by LOADING it — re-mount the editor there. */
117
+ load(target: EditorTarget): void;
118
+ /** Reload the editor from scratch — re-mount it on the same target. */
119
+ reload(): void;
120
+ }
121
+
122
+ export interface EditorHost {
123
+ readonly connected: boolean;
124
+ readonly ownsDocument: boolean;
125
+ send: SendToHost;
126
+ subscribe(listener: (message: InboundHostMessage) => void): () => void;
127
+ readonly address: EditorHostAddress;
128
+ }
129
+
130
+ /** A host for an editor mounted in the embedder's own page. One per editor. */
131
+ export function createEditorHost(options: {
132
+ onMessage: (message: EditorToHostMessage) => void;
133
+ address: EditorHostAddress;
134
+ }): { host: EditorHost; deliver: (message: HostToEditorMessage) => void };
135
+
136
+ // ── The backend (0.4.0) ──────────────────────────────────────────────────────
137
+ //
138
+ // Everything the editor asks a server for, as one contract an embedder may
139
+ // implement. Leave `backend` out of the editor's props and it is EFFICIENT's
140
+ // (`createEfficientBackend`), authorised by the token the host delivers with
141
+ // `storefront-editor:auth` — exactly as before 0.4.0. Supply one and the editor
142
+ // waits for no token, draws no control the backend has no group for, and makes
143
+ // no request of its own.
144
+ //
145
+ // • `pages`, `site.get` and `media.resolve` are required; every other group
146
+ // and method is optional, and ABSENCE IS THE ANSWER — never implement a
147
+ // method to say "not supported".
148
+ // • A failed call rejects with `WebsiteBackendError`; the editor reads its
149
+ // `kind`, never a status code or a response body.
150
+ // • No credential crosses the interface: how a call is authorised is the
151
+ // backend's own business.
152
+
153
+ /** The page document the editor edits: the engine's `Data` (`content`, `root`,
154
+ * `zones`), stored and returned exactly as it was saved. Opaque to a backend. */
155
+ export type PageLayout = Record<string, unknown>;
156
+
157
+ /** One row of the editor's page list. Rows arrive in display order. */
158
+ export type EditorPageRow = {
159
+ id: string;
160
+ slug: string;
161
+ /** What the list calls the page. */
162
+ label: string;
163
+ /** Header text of the row's group (null / absent = top level). */
164
+ group?: string | null;
165
+ /** Stable key of that group — two groups may share a header text. */
166
+ groupKey?: string;
167
+ /** Not published yet (drives the "draft" tag). False where pages never publish. */
168
+ draft: boolean;
169
+ /** Page kind, where the backend has kinds (`dynamic`, `lightbox`, …). */
170
+ kind?: string | null;
171
+ };
172
+
173
+ /**
174
+ * A page's content, as it is stored and saved.
175
+ *
176
+ * `translations` and `seo` are RESERVED for per-language content: a backend
177
+ * that stores a page as this bundle from the start does not migrate its storage
178
+ * when the multilingual overlay ships. 0.4.0 edits `layout` only — it sends
179
+ * neither of the other two on a save, and a backend treats an absent member as
180
+ * "leave what is stored".
181
+ */
182
+ export type PageBundle = {
183
+ layout: PageLayout;
184
+ /** Per-language overrides of the layout's text, keyed by language tag. */
185
+ translations?: Record<string, unknown>;
186
+ /** Per-language title and description, keyed by language tag. */
187
+ seo?: Record<string, { title?: string; description?: string }>;
188
+ };
189
+
190
+ /** A page as the editor opens it. */
191
+ export type PageDocument = PageBundle & {
192
+ id: string;
193
+ /**
194
+ * The VERSION of the stored document this copy is based on — what the next
195
+ * save says it replaces. Any string the backend can compare for equality; the
196
+ * editor passes it back untouched. `null` means the backend keeps no versions
197
+ * for this document: every save is accepted and nothing is ever refused as
198
+ * `changed-elsewhere`.
199
+ */
200
+ version: string | null;
201
+ /** Page kind, where the backend has kinds. */
202
+ kind?: string | null;
203
+ };
204
+
205
+ /** The website a call is about, when the editor's target names one
206
+ * (`EditorTarget.siteId`). A backend that serves one website ignores it. */
207
+ export type SiteScope = { siteId?: string };
208
+
209
+ /** A page by its slug or its id. */
210
+ export type PageRef = ({ slug: string } | { id: string }) & SiteScope;
211
+
212
+ /** What the page list's inline "Add page" hands the backend. */
213
+ export type NewPageInput = { slug: string; title: string; layout: PageLayout } & SiteScope;
214
+
215
+ /** What a save says it expects, and who is writing. */
216
+ export type SaveExpectation = {
217
+ /** The version being replaced (`PageDocument.version`, then each save's own). */
218
+ version: string | null;
219
+ /** This editor tab — the id its lock claim sends. */
220
+ clientId: string;
221
+ };
222
+
223
+ /** A set of pages: a website's, or one template's. */
224
+ /**
225
+ * A page a link can point at: enough to name it, and its layout — where its
226
+ * section anchors are read from, and how the editor tells which page holds a
227
+ * section a link may open in a particular state.
228
+ */
229
+ export interface LinkTargetPage {
230
+ id: string;
231
+ /** The page's handle. A link to the page is built from this. */
232
+ slug: string;
233
+ /** The page's name as a list shows it. */
234
+ label: string;
235
+ /** The page's layout, or null when the backend does not hand it out with the
236
+ * list. Without it the page is still a link target; its anchors are not. */
237
+ layout: PageLayout | null;
238
+ }
239
+
240
+ export interface WebsitePages {
241
+ /** The rows the page list navigates between. */
242
+ list(scope?: SiteScope): Promise<EditorPageRow[]>;
243
+ /** The page, or null when there is none. */
244
+ load(ref: PageRef): Promise<PageDocument | null>;
245
+ /**
246
+ * Store the page's working copy and answer with the version it now has.
247
+ * Reject `changed-elsewhere` when `expect.version` is no longer the stored
248
+ * one, and `lock-taken` when somebody took the page from `expect.clientId`.
249
+ */
250
+ save(id: string, doc: PageBundle, expect: SaveExpectation): Promise<{ version: string | null }>;
251
+ /** The stored version alone, without the layout. Lets the editor re-base a
252
+ * save its own other tab refused, and reload after a take-over only when the
253
+ * page really moved. */
254
+ version?(id: string): Promise<string | null>;
255
+ /** Make the stored working copy the public one. Without it the editor's main
256
+ * button reads "Save" and nothing is ever a draft. */
257
+ publish?(id: string, expect?: { version: string }): Promise<{ version: string | null }>;
258
+ /** Add a page from the page list. Without it the editor asks its HOST for a
259
+ * new page (`storefront-editor:new-page`). */
260
+ create?(input: NewPageInput): Promise<EditorPageRow>;
261
+ /** Delete a page from the page list. */
262
+ remove?(id: string): Promise<void>;
263
+ /**
264
+ * Every page of the website a link may point at, each with its layout — in
265
+ * ONE call, for the link picker. Optional: without it the picker lists the
266
+ * rows of {@link list} and offers no section anchors.
267
+ */
268
+ linkTargets?(scope?: SiteScope): Promise<LinkTargetPage[]>;
269
+ }
270
+
271
+ /** The other operator, as the editor's notices describe them. */
272
+ export type LockHolder = {
273
+ name: string;
274
+ /** Seconds since their last heartbeat, as the SERVER measured it. */
275
+ secondsSinceSeen: number;
276
+ /** Seconds since they TOOK the page, as the SERVER measured it. */
277
+ secondsSinceHeld: number;
278
+ };
279
+
280
+ /** A refused save: the stored page moved on since this editor loaded it. */
281
+ export type SaveConflict = {
282
+ /** Who saved. Empty when the backend cannot attribute the write. */
283
+ savedBy: string;
284
+ /** True when it was this same person, in another tab. */
285
+ savedByYou: boolean;
286
+ secondsAgo: number;
287
+ };
288
+
289
+ /** The advisory "somebody is editing this page" lock. */
290
+ export interface WebsiteEditLock {
291
+ /**
292
+ * Claim the page, refresh the claim (the heartbeat) or — `takeover` — take it
293
+ * from whoever holds it. Resolves once this tab holds the page; rejects
294
+ * `lock-taken` (with `holder`) while somebody else does.
295
+ */
296
+ claim(id: string, clientId: string, takeover: boolean): Promise<{ heartbeatSeconds: number }>;
297
+ /** Hand the page back. Best-effort: a lock expires by itself. */
298
+ release(id: string, clientId: string): Promise<void>;
299
+ }
300
+
301
+ /** What the editor reads of the website itself. */
302
+ export interface SiteRecord {
303
+ /** The website's display name. */
304
+ name: string;
305
+ /** Its public slug, where it has one. */
306
+ slug?: string | null;
307
+ /** The site-wide Google Maps browser key the canvas's map blocks draw with. */
308
+ mapsApiKey?: string | null;
309
+ /** The site-wide Google Maps cloud style. */
310
+ mapsMapId?: string | null;
311
+ }
312
+
313
+ /** The part of the site record the editor writes. */
314
+ export type SiteRecordPatch = { mapsApiKey?: string };
315
+
316
+ export interface WebsiteSite {
317
+ get(scope?: SiteScope): Promise<SiteRecord>;
318
+ /** Save the site-wide Maps key. Without it that field is read-only. */
319
+ update?(patch: SiteRecordPatch, scope?: SiteScope): Promise<SiteRecord>;
320
+ /** RESERVED for the multilingual overlay: the languages the website is
321
+ * written in. 0.4.0 does not call it. */
322
+ languages?(): Promise<{ base: string; others: string[] }>;
323
+ }
324
+
325
+ export interface WebsiteMedia {
326
+ /**
327
+ * Turn a stored media reference into a URL the browser can load, or answer
328
+ * null to leave the reference as it is. Synchronous — it runs while a section
329
+ * renders. `width` is the size a thumbnail is drawn at, when the caller has one.
330
+ */
331
+ resolve(ref: string, options?: { width?: number }): string | null;
332
+ /** RESERVED: upload a file dropped on a media field. 0.4.0 hands a dropped
333
+ * file to the host's media panel or picker and does not call it. */
334
+ upload?(file: File): Promise<HostPickedMedia>;
335
+ /**
336
+ * The largest file a media field takes, in bytes. A larger one is refused
337
+ * where it was dropped, with the reason, before it is handed to the host's
338
+ * media panel or picker. Without it the editor refuses no file for its size —
339
+ * the host's own picker has the say.
340
+ */
341
+ maxUploadBytes?: number;
342
+ }
343
+
344
+ /** One node of a page document: a section or a block, with its own props. */
345
+ export type SectionNode = { type: string; props: { id: string; [key: string]: unknown } };
346
+
347
+ /** A stored section template. `node` is null for a card nobody has designed. */
348
+ export interface SectionTemplateRecord {
349
+ id: string;
350
+ name: string;
351
+ component_type: string;
352
+ node: SectionNode | null;
353
+ created_at: string;
354
+ updated_at: string;
355
+ }
356
+
357
+ /** A section template with a design. */
358
+ export interface SectionTemplate extends SectionTemplateRecord {
359
+ node: SectionNode;
360
+ }
361
+
362
+ /** Reusable single sections, edited on their own and dropped into any page. */
363
+ export interface WebsiteSectionTemplates {
364
+ /** The designed templates, newest first. */
365
+ list(): Promise<SectionTemplate[]>;
366
+ /** The record at `id`, designed or not; null when there is none. */
367
+ get(id: string): Promise<SectionTemplateRecord | null>;
368
+ /** The template at `id` with its design, seeding `name` + `node` when it has none. */
369
+ ensure(id: string, name: string, node: SectionNode): Promise<SectionTemplate>;
370
+ save(name: string, node: SectionNode): Promise<SectionTemplate>;
371
+ update(id: string, node: SectionNode): Promise<SectionTemplate>;
372
+ rename(id: string, name: string): Promise<SectionTemplateRecord>;
373
+ remove(id: string): Promise<void>;
374
+ }
375
+
376
+ /** Website templates: blueprints whose pages the same editor edits. */
377
+ export interface WebsiteTemplates {
378
+ /** The template's name (the canvas header's wordmark while it is edited). */
379
+ get(id: string): Promise<{ id: string; name: string }>;
380
+ /** The template's pages. They never publish and carry no lock. */
381
+ pages(templateId: string): WebsitePages;
382
+ }
383
+
384
+ /** Where the top bar's Preview link goes. */
385
+ export interface WebsitePreview {
386
+ /** The public address of the page with this slug. */
387
+ href(slug: string): string;
388
+ }
389
+
390
+ /**
391
+ * A blog post, as the editor lists one: enough to name it, link to it and draw a
392
+ * card from it. Published posts only — a draft is not something a page links to.
393
+ */
394
+ export interface BlogPostRow {
395
+ /** The post's stable handle. A block that is linked to a post stores this. */
396
+ slug: string;
397
+ title: string;
398
+ /** The standfirst. Empty when the post has none. */
399
+ excerpt: string;
400
+ /** The post's editorial category, as its author typed it. Empty when none. */
401
+ category: string;
402
+ /** The cover picture's address, absolute — or "" when the post has none. */
403
+ coverImage: string;
404
+ /** When it was published (ISO 8601), or null. */
405
+ publishedAt: string | null;
406
+ /** Where the post lives on the website, as a path (`/journal/first-post`). */
407
+ href: string;
408
+ }
409
+
410
+ /**
411
+ * The website's blog. Optional: without it the editor lists no post — a block's
412
+ * "Blog post source" shows only what it is already linked to, and the link
413
+ * picker offers no "Blog post" (`EditorCapabilities.hasBlog`).
414
+ */
415
+ export interface WebsiteBlog {
416
+ /** The website's published posts, in the order the website lists them. */
417
+ posts(scope?: SiteScope): Promise<BlogPostRow[]>;
418
+ }
419
+
420
+ /** The website's live-chat bubble: whether it shows, and its three lines.
421
+ * A blank line means "the widget's own wording". */
422
+ export interface ChatSettings {
423
+ enabled: boolean;
424
+ title: string;
425
+ subtitle: string;
426
+ greeting: string;
427
+ }
428
+
429
+ /**
430
+ * The website's live chat. Optional: without it the editor's "Live chat" panel
431
+ * previews a change and saves nothing, and starts from the house default.
432
+ */
433
+ export interface WebsiteChat {
434
+ get(scope?: SiteScope): Promise<ChatSettings>;
435
+ /** Store the lines that changed; answer the settings as they now stand. */
436
+ update(patch: Partial<ChatSettings>, scope?: SiteScope): Promise<ChatSettings>;
437
+ }
438
+
439
+ /**
440
+ * A website's appearance as it is stored: its colour themes, type, corners,
441
+ * cards and widths — the row behind the Site styles panel. The keys are the
442
+ * stored ones. Every key but `id` and the first run is optional or nullable:
443
+ * absent and `null` both mean "not set", and the website draws the built-in
444
+ * look for it.
445
+ */
446
+ export interface SiteThemeRow {
447
+ id: string;
448
+ selected_preset: number;
449
+ theme_presets: {
450
+ text: string;
451
+ accent: string;
452
+ background?: string;
453
+ surface?: string;
454
+ line?: string;
455
+ muted?: string;
456
+ button?: string;
457
+ buttonInk?: string;
458
+ }[];
459
+ accent_color?: string | null;
460
+ heading_font: string;
461
+ body_font: string;
462
+ accent_font: string;
463
+ label_font: string;
464
+ default_mode: "light" | "dark" | "auto";
465
+ show_theme_switch: boolean;
466
+ heading_scale: number;
467
+ body_scale: number;
468
+ label_scale: number;
469
+ heading_size_large_px?: number | null;
470
+ heading_size_medium_px?: number | null;
471
+ heading_size_small_px?: number | null;
472
+ accent_size_px?: number | null;
473
+ body_size_px?: number | null;
474
+ label_size_px?: number | null;
475
+ corner_style: "square" | "small" | "large";
476
+ corner_radius_px?: number | null;
477
+ heading_compression: "normal" | "tight" | "compressed";
478
+ card_shadow?: "none" | "soft" | "lifted" | null;
479
+ shadow_color?: string | null;
480
+ card_gap: "tight" | "normal" | "roomy";
481
+ hover_effect?: "none" | "lift" | "highlight" | null;
482
+ heading_case?: "authored" | "sentence" | null;
483
+ heading_weight?: number | null;
484
+ heading_weight_strong?: number | null;
485
+ heading_2_weight?: number | null;
486
+ heading_3_weight?: number | null;
487
+ heading_2_case?: "authored" | "sentence" | null;
488
+ heading_3_case?: "authored" | "sentence" | null;
489
+ heading_max_px?: number | null;
490
+ site_width?: number | null;
491
+ site_gutter?: number | null;
492
+ block_gap?: number | null;
493
+ hero_background_follows_width?: boolean | null;
494
+ photography_grayscale?: boolean | null;
495
+ word_styles?: Partial<Record<string, Record<string, unknown> | null>> | null;
496
+ }
497
+
498
+ /** What may be written: any of a {@link SiteThemeRow} but its own id. */
499
+ export type SiteThemeRowPatch = Partial<Omit<SiteThemeRow, "id">>;
500
+
501
+ /**
502
+ * The website's appearance. Optional: without it the editor has no stored
503
+ * appearance to open on or save to — no Site styles — and a website is drawn
504
+ * in the built-in look.
505
+ */
506
+ export interface WebsiteTheme {
507
+ /** The row behind a website — its own, or the one it falls back to. Reading
508
+ * must not create one. */
509
+ get(scope?: SiteScope): Promise<SiteThemeRow>;
510
+ /** Store the keys that changed; answer the row as it now stands. */
511
+ update(patch: SiteThemeRowPatch, scope?: SiteScope): Promise<SiteThemeRow>;
512
+ }
513
+
514
+ /** The website's social profile links: one address per platform, under the
515
+ * platform's own name (`instagram`, `facebook`, …). "" where there is none. */
516
+ export type SocialLinkSettings = Record<string, string>;
517
+
518
+ /**
519
+ * The website's social links. Optional: without it the editor's Social links
520
+ * rows — and a footer's Social block — show no saved link and save none.
521
+ */
522
+ export interface WebsiteSocial {
523
+ get(scope?: SiteScope): Promise<SocialLinkSettings>;
524
+ /**
525
+ * Store the links named; answer every link as it now stands. Reject
526
+ * `invalid` with `fields` — the platform refused, and the reason to show
527
+ * beside its box — when an address is not one.
528
+ */
529
+ update(patch: SocialLinkSettings, scope?: SiteScope): Promise<SocialLinkSettings>;
530
+ }
531
+
532
+ /**
533
+ * Everything the editor asks a server for.
534
+ *
535
+ * Other groups the editor still reads from the EFFICIENT client directly — the
536
+ * site's header, menus, forms and the catalog — join this interface as
537
+ * optional members when their call sites move. Until then a backend other than
538
+ * EFFICIENT's gets the page editor without them. The blog (`blog`), the live
539
+ * chat (`chat`), the social links (`social`) and the site's appearance
540
+ * (`theme`) have crossed.
541
+ */
542
+ export interface WebsiteBackend {
543
+ pages: WebsitePages;
544
+ lock?: WebsiteEditLock;
545
+ site: WebsiteSite;
546
+ media: WebsiteMedia;
547
+ sectionTemplates?: WebsiteSectionTemplates;
548
+ templates?: WebsiteTemplates;
549
+ preview?: WebsitePreview;
550
+ blog?: WebsiteBlog;
551
+ chat?: WebsiteChat;
552
+ social?: WebsiteSocial;
553
+ theme?: WebsiteTheme;
554
+ }
555
+
556
+ /** Why a backend call failed, in the editor's own terms. */
557
+ export type WebsiteBackendErrorKind =
558
+ /** The session is not (or no longer) authorised. The editor tells its host
559
+ * (`storefront-editor:auth-expired`) and treats the call as failed. */
560
+ | "unauthorized"
561
+ /** A save was refused: the stored page moved on since this copy was loaded.
562
+ * Carries `conflict`. */
563
+ | "changed-elsewhere"
564
+ /** Somebody else holds the page (a claim), or took it from this tab (a save).
565
+ * Carries `holder`. */
566
+ | "lock-taken"
567
+ /** There is no such page, site or template. */
568
+ | "not-found"
569
+ /** The backend understood the request and refused it as wrong. */
570
+ | "invalid"
571
+ /** No answer: offline, timed out, the backend itself failing. */
572
+ | "unavailable";
573
+
574
+ /**
575
+ * The one error a backend rejects with. `message` is shown to the operator
576
+ * where the editor has nowhere better to read from (a document that would not
577
+ * load), so write it for a person.
578
+ */
579
+ export class WebsiteBackendError extends Error {
580
+ readonly kind: WebsiteBackendErrorKind;
581
+ /** `changed-elsewhere`: who saved, and whether it was this person's own tab. */
582
+ readonly conflict?: SaveConflict;
583
+ /** `lock-taken`: who holds the page. */
584
+ readonly holder?: LockHolder;
585
+ /** `invalid`: the inputs that were refused, by name, each with the reason —
586
+ * when the backend can say which. The editor shows it beside that input. */
587
+ readonly fields?: Readonly<Record<string, string>>;
588
+ constructor(
589
+ kind: WebsiteBackendErrorKind,
590
+ message?: string,
591
+ detail?: { conflict?: SaveConflict; holder?: LockHolder; fields?: Readonly<Record<string, string>>; cause?: unknown },
592
+ );
593
+ }
594
+
595
+ export interface EfficientBackendOptions {
596
+ /** Pin every site-scoped call to one website. Without it the client follows
597
+ * the editor's open document, as the built-in default does. */
598
+ siteId?: string;
599
+ }
600
+
601
+ /**
602
+ * The EFFICIENT backend, for an embedder that wants to pass it explicitly — to
603
+ * hand over one group of its own: `{ ...createEfficientBackend(), preview }`.
604
+ * Leaving `backend` out of the editor's props is the same thing.
605
+ */
606
+ export function createEfficientBackend(options?: EfficientBackendOptions): WebsiteBackend;
607
+
608
+ /** The website the editor is showing. */
609
+ export interface WebsiteEditorSite {
610
+ /** `StorefrontSite.slug` — named on every public read. */
611
+ slug: string;
612
+ /** The website's public origin, no trailing slash. */
613
+ origin: string;
614
+ }
615
+
616
+ // ── Which blocks the editor offers (0.7.0) ───────────────────────────────────
617
+ //
618
+ // Leave `blocks` out of the editor's props and it offers every block it has —
619
+ // exactly as before 0.7.0. Supply it to choose which SETS are offered, to allow
620
+ // or deny single blocks, and to add blocks of your own.
621
+ //
622
+ // • OFFERED, NOT REGISTERED. A block you leave out is in no group of the Add
623
+ // panel, no slot's menu and no drop target. A page that already holds one
624
+ // still draws it, and it can be selected and deleted.
625
+ // • NOT A SECURITY BOUNDARY. Check a saved page on your server with
626
+ // `blockTypesIn` from `efficient-storefront-blocks/contract`.
627
+ // • `sets`, `allow`, `deny`, `host` and `categories` are read once, when the
628
+ // editor mounts — re-mount (a `key`) to change them. `metadata` is live.
629
+
630
+ /**
631
+ * The set a block travels in.
632
+ *
633
+ * general draws from its own props and what the host hands it; any host
634
+ * may offer it
635
+ * commerce the shop: products, the catalogue, the cart, the checkout
636
+ * service needs one of the EFFICIENT backend's site services (forms,
637
+ * bookings, reviews, the newsletter, the blog, menus)
638
+ */
639
+ export type WebsiteBlockSet = "general" | "commerce" | "service";
640
+
641
+ /** A group of the Add panel a host places its blocks in. */
642
+ export interface WebsiteBlockCategory {
643
+ /** The group's key. An existing group's key (`content`, `structure`, …) adds
644
+ * the blocks to the end of that group; a new key makes a new group. */
645
+ key: string;
646
+ /** A new group's heading. Not read for an existing group. */
647
+ title: string;
648
+ /** The types in the group, in order. */
649
+ components: string[];
650
+ /** Put a NEW group before the group with this key. Without it — or when no
651
+ * such group is offered — the group is added last. */
652
+ before?: string;
653
+ }
654
+
655
+ /** The host's choice of blocks. Every member is optional. */
656
+ export interface WebsiteEditorBlocks {
657
+ /** The sets offered. Default: all three. */
658
+ sets?: readonly WebsiteBlockSet[];
659
+ /** After `sets`: only these block types. A section that is built from other
660
+ * blocks (a Hero from its slides) is offered only when those are too. */
661
+ allow?: readonly string[];
662
+ /** After both: never these. */
663
+ deny?: readonly string[];
664
+ /**
665
+ * The host's own blocks, by type, each written as the engine's
666
+ * `ComponentConfig`. A type the editor already registers is refused (the
667
+ * editor throws when it mounts): name yours so they cannot collide.
668
+ *
669
+ * A host's block is a SECTION: it is added to the page from the Add panel, in
670
+ * the group `categories` names, and it is given what the editor's own
671
+ * sections are given — the Padding, Corners, Width, Height, Anchor, Theme and
672
+ * Motion rows, the settings tabs (its own fields under Content),
673
+ * click-to-edit text and the Layers tree's eye. So untouched it is on the
674
+ * site's first theme, as any section is; and a field of its own under a name
675
+ * the editor adds a row with (`anchor`, `widthMode`, `sectionAccent`,
676
+ * `sectionPaddingTop`, …) is refused when the editor mounts. It is not
677
+ * offered inside a Box.
678
+ */
679
+ host?: Record<string, ComponentConfig>;
680
+ /** Where the host's blocks stand in the Add panel. A block named in no group
681
+ * is registered and drawn, and offered nowhere. */
682
+ categories?: WebsiteBlockCategory[];
683
+ /**
684
+ * The host's live data. Every block in the canvas receives it as
685
+ * `puck.metadata`. Change the object and the blocks redraw in place; it is
686
+ * never written to the page or to the undo history.
687
+ */
688
+ metadata?: Record<string, unknown>;
689
+ }
690
+
691
+ /** The page / website-template editor. Re-mount (a `key`) to change website. */
692
+ export function WebsiteEditor(props: {
693
+ host: EditorHost;
694
+ target: EditorTarget;
695
+ site: WebsiteEditorSite | null;
696
+ /** The host's media library, drawn in the editor's left panel. Without one,
697
+ * fields ask the host to open its picker window (`open-media-picker`). */
698
+ mediaPanel?: MediaPanelComponent | null;
699
+ /** The server the editor talks to. Without one it is EFFICIENT's
700
+ * (`createEfficientBackend`). Read once, when the editor mounts. */
701
+ backend?: WebsiteBackend | null;
702
+ /** Which blocks the editor offers, and the host's own. Without one: every
703
+ * block, as before 0.7.0. */
704
+ blocks?: WebsiteEditorBlocks | null;
705
+ }): ReactElement;
706
+
707
+ /** The standalone one-section editor (a section template). */
708
+ export function WebsiteSectionEditor(props: {
709
+ host: EditorHost;
710
+ id: string;
711
+ initialType?: string;
712
+ initialName?: string;
713
+ site: WebsiteEditorSite | null;
714
+ /** The host's media library — see `WebsiteEditor`. */
715
+ mediaPanel?: MediaPanelComponent | null;
716
+ /** The server the editor talks to — see `WebsiteEditor`. Needs
717
+ * `sectionTemplates`. */
718
+ backend?: WebsiteBackend | null;
719
+ /** Which blocks the editor offers — see `WebsiteEditor`. */
720
+ blocks?: WebsiteEditorBlocks | null;
721
+ }): ReactElement;