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.
- package/CHANGELOG.md +397 -0
- package/LICENSE +10 -0
- package/README.md +279 -2
- package/dist/chunks/EditorPreviewCheckout-J27HMNBW.js +4 -0
- package/dist/chunks/PreviewFooter-BMBCVZQC.js +1 -0
- package/dist/chunks/SectionEditorShell-K4OEOAY4.js +1 -0
- package/dist/chunks/SiteEditorProvider-ECGZVKLG.js +1 -0
- package/dist/chunks/StorefrontEditorChrome-T6DTL7T7.js +1 -0
- package/dist/chunks/chunk-3J4FEDNZ.js +2 -0
- package/dist/chunks/chunk-6CP4D6VJ.js +1 -0
- package/dist/chunks/chunk-6XRKNA2E.js +1 -0
- package/dist/chunks/chunk-C2CHF6AL.js +15 -0
- package/dist/chunks/chunk-C2CHF6AL.js.LEGAL.txt +27 -0
- package/dist/chunks/chunk-ENDWECYV.js +1 -0
- package/dist/chunks/chunk-HVOO2GNE.js +1 -0
- package/dist/chunks/chunk-MYSIADXX.js +1 -0
- package/dist/chunks/chunk-NJXX5BI2.js +1 -0
- package/dist/chunks/chunk-RRHBLOH4.js +1 -0
- package/dist/chunks/chunk-TGIEMCNA.js +1 -0
- package/dist/chunks/chunk-THATLL5Z.js +230 -0
- package/dist/chunks/chunk-TOY67YUY.js +153 -0
- package/dist/chunks/chunk-TOY67YUY.js.LEGAL.txt +4 -0
- package/dist/chunks/chunk-UBWM4USZ.js +1 -0
- package/dist/chunks/chunk-Y6SLVHK3.js +1 -0
- package/dist/chunks/fitment-data-BXI7KQRZ.js +1 -0
- package/dist/chunks/gallery-builds-data-SWA6XKFF.js +1 -0
- package/dist/chunks/headers-VF4VTX37.js +1 -0
- package/dist/chunks/render-client-UQ5CUPRL.js +1 -0
- package/dist/contract.d.ts +48 -0
- package/dist/contract.js +1 -0
- package/dist/editor.css +1 -0
- package/dist/index.d.ts +721 -0
- package/dist/index.js +11361 -0
- package/dist/links.d.ts +86 -0
- package/dist/links.js +1 -0
- package/package.json +54 -3
package/dist/index.d.ts
ADDED
|
@@ -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;
|