@wocart/theme 2.3.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/README.md +24 -1077
- package/dist/blocks.d.ts +20 -2
- package/dist/blocks.js +41 -7
- package/dist/composition.d.ts +1 -1
- package/dist/composition.js +4 -2
- package/dist/definitions.js +28 -5
- package/dist/image-service.d.ts +16 -19
- package/dist/image-service.js +20 -23
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/manifest.d.ts +3 -2
- package/dist/manifest.js +4 -2
- package/dist/operations.d.ts +5 -0
- package/dist/operations.js +26 -1
- package/dist/page-data.js +24 -3
- package/dist/preview-target.d.ts +3 -0
- package/dist/preview-target.js +9 -4
- package/dist/types.d.ts +12 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +3 -3
package/dist/blocks.d.ts
CHANGED
|
@@ -19,12 +19,30 @@ export declare const THEME_LIMITS: {
|
|
|
19
19
|
};
|
|
20
20
|
export declare const COMPONENT_PLACEMENTS: readonly ["section", "block", "both"];
|
|
21
21
|
export declare const COMPONENT_ROLES: readonly ["order-form"];
|
|
22
|
+
/** The surface every landing page's own document follows: its rules, its shipped document. */
|
|
23
|
+
export declare const LANDING_TEMPLATE = "landing";
|
|
22
24
|
/** The surface an area's document is stored under: `area:<key>`. */
|
|
23
25
|
export declare function areaSurface(key: string): string;
|
|
24
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The surface a landing page's own document is stored under: `lp:<id>`, by
|
|
28
|
+
* the page's id, which stays when its slug changes. A landing page is a
|
|
29
|
+
* campaign of its own, so each has its own document; the theme's `landing`
|
|
30
|
+
* surface gives every one of them its rules and its starting document.
|
|
31
|
+
*/
|
|
32
|
+
export declare function landingSurface(id: string): string;
|
|
33
|
+
/** The surface whose rules and shipped document a document follows: `landing` for a landing page's own `lp:<id>`, the surface itself otherwise. */
|
|
34
|
+
export declare function templateSurface(surface: string): string;
|
|
35
|
+
/**
|
|
36
|
+
* The shipped document for a surface or an area, when the theme declares one.
|
|
37
|
+
* A landing page's own surface gets the `landing` document, addressed to it.
|
|
38
|
+
*/
|
|
25
39
|
export declare function shippedDocument(theme: ThemeDefinition, surface: string): PageData | undefined;
|
|
26
|
-
/** Whether a theme composes a surface: one it declares, or one of its areas. */
|
|
40
|
+
/** Whether a theme composes a surface: one it declares, a landing page's own when it declares `landing`, or one of its areas. */
|
|
27
41
|
export declare function composesSurface(theme: ThemeDefinition, surface: string): boolean;
|
|
42
|
+
/** The areas a page of this surface may leave out: the surface's `hideableAreas` the theme declares as areas. */
|
|
43
|
+
export declare function hideableAreas(theme: ThemeDefinition, surface: string): string[];
|
|
44
|
+
/** Whether a page shows one of the theme's areas: it does unless its document leaves it out and its surface allows that. */
|
|
45
|
+
export declare function isAreaShown(theme: ThemeDefinition, page: PageData, key: string): boolean;
|
|
28
46
|
/** The id a fixed block renders with while the document does not store it. */
|
|
29
47
|
export declare function fixedBlockId(ownerId: string, name: string): string;
|
|
30
48
|
/** Whether a component may be placed at the top level (`"section"`) or in a slot (`"block"`). */
|
package/dist/blocks.js
CHANGED
|
@@ -12,19 +12,52 @@ export const THEME_LIMITS = {
|
|
|
12
12
|
export const COMPONENT_PLACEMENTS = ["section", "block", "both"];
|
|
13
13
|
export const COMPONENT_ROLES = ["order-form"];
|
|
14
14
|
const AREA_PREFIX = "area:";
|
|
15
|
+
const LANDING_PREFIX = "lp:";
|
|
16
|
+
/** The surface every landing page's own document follows: its rules, its shipped document. */
|
|
17
|
+
export const LANDING_TEMPLATE = "landing";
|
|
15
18
|
/** The surface an area's document is stored under: `area:<key>`. */
|
|
16
19
|
export function areaSurface(key) {
|
|
17
20
|
return `${AREA_PREFIX}${key}`;
|
|
18
21
|
}
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The surface a landing page's own document is stored under: `lp:<id>`, by
|
|
24
|
+
* the page's id, which stays when its slug changes. A landing page is a
|
|
25
|
+
* campaign of its own, so each has its own document; the theme's `landing`
|
|
26
|
+
* surface gives every one of them its rules and its starting document.
|
|
27
|
+
*/
|
|
28
|
+
export function landingSurface(id) {
|
|
29
|
+
return `${LANDING_PREFIX}${id}`;
|
|
30
|
+
}
|
|
31
|
+
/** The surface whose rules and shipped document a document follows: `landing` for a landing page's own `lp:<id>`, the surface itself otherwise. */
|
|
32
|
+
export function templateSurface(surface) {
|
|
33
|
+
return surface.startsWith(LANDING_PREFIX) ? LANDING_TEMPLATE : surface;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The shipped document for a surface or an area, when the theme declares one.
|
|
37
|
+
* A landing page's own surface gets the `landing` document, addressed to it.
|
|
38
|
+
*/
|
|
20
39
|
export function shippedDocument(theme, surface) {
|
|
21
40
|
if (surface.startsWith(AREA_PREFIX))
|
|
22
41
|
return (theme.areas ?? []).find((area) => areaSurface(area.key) === surface)?.document;
|
|
23
|
-
|
|
42
|
+
const template = templateSurface(surface);
|
|
43
|
+
const document = (theme.surfaces ?? []).find((spec) => spec.key === template)?.document;
|
|
44
|
+
return document === undefined || template === surface ? document : { ...document, surface };
|
|
24
45
|
}
|
|
25
|
-
/** Whether a theme composes a surface: one it declares, or one of its areas. */
|
|
46
|
+
/** Whether a theme composes a surface: one it declares, a landing page's own when it declares `landing`, or one of its areas. */
|
|
26
47
|
export function composesSurface(theme, surface) {
|
|
27
|
-
|
|
48
|
+
const template = templateSurface(surface);
|
|
49
|
+
return (theme.surfaces ?? []).some((spec) => spec.key === template) || (theme.areas ?? []).some((area) => areaSurface(area.key) === surface);
|
|
50
|
+
}
|
|
51
|
+
/** The areas a page of this surface may leave out: the surface's `hideableAreas` the theme declares as areas. */
|
|
52
|
+
export function hideableAreas(theme, surface) {
|
|
53
|
+
const template = templateSurface(surface);
|
|
54
|
+
const spec = (theme.surfaces ?? []).find((candidate) => candidate.key === template);
|
|
55
|
+
const declared = new Set((theme.areas ?? []).map((area) => area.key));
|
|
56
|
+
return (spec?.hideableAreas ?? []).filter((key) => declared.has(key));
|
|
57
|
+
}
|
|
58
|
+
/** Whether a page shows one of the theme's areas: it does unless its document leaves it out and its surface allows that. */
|
|
59
|
+
export function isAreaShown(theme, page, key) {
|
|
60
|
+
return !(page.hiddenAreas ?? []).includes(key) || !hideableAreas(theme, page.surface).includes(key);
|
|
28
61
|
}
|
|
29
62
|
/** The id a fixed block renders with while the document does not store it. */
|
|
30
63
|
export function fixedBlockId(ownerId, name) {
|
|
@@ -46,9 +79,10 @@ export function surfaceRules(theme, surface) {
|
|
|
46
79
|
return { ...(area?.required === undefined ? {} : { required: area.required }), ...(area?.max === undefined ? {} : { max: area.max }) };
|
|
47
80
|
}
|
|
48
81
|
const surfaces = theme.surfaces ?? [];
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const
|
|
82
|
+
const key = templateSurface(surface);
|
|
83
|
+
const own = surfaces.find((spec) => spec.key === key);
|
|
84
|
+
const dot = key.indexOf(".");
|
|
85
|
+
const base = dot > 0 ? surfaces.find((spec) => spec.key === key.slice(0, dot)) : undefined;
|
|
52
86
|
const required = own?.required ?? base?.required;
|
|
53
87
|
const max = own?.max ?? base?.max;
|
|
54
88
|
return { ...(required === undefined ? {} : { required }), ...(max === undefined ? {} : { max }) };
|
package/dist/composition.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentDefinition, ComponentInstance, ComponentSpec, ThemeDefinition } from "./types.js";
|
|
2
|
-
/** Whether a surface key is `entry` itself or one of its alternates (`product.compact` for `product`)
|
|
2
|
+
/** Whether a surface key is `entry` itself or one of its alternates (`product.compact` for `product`); a landing page's own `lp:<id>` is covered by `landing`. */
|
|
3
3
|
export declare function surfaceCovers(entry: string, surface: string): boolean;
|
|
4
4
|
/**
|
|
5
5
|
* Whether an editor may insert this component on `surface`: it is not marked
|
package/dist/composition.js
CHANGED
|
@@ -3,10 +3,12 @@
|
|
|
3
3
|
* where a component may go, and how a preset becomes instances. Pure: no
|
|
4
4
|
* I/O, no randomness — ids come from the caller.
|
|
5
5
|
*/
|
|
6
|
+
import { templateSurface } from "./blocks.js";
|
|
6
7
|
import { createInstance } from "./instances.js";
|
|
7
|
-
/** Whether a surface key is `entry` itself or one of its alternates (`product.compact` for `product`)
|
|
8
|
+
/** Whether a surface key is `entry` itself or one of its alternates (`product.compact` for `product`); a landing page's own `lp:<id>` is covered by `landing`. */
|
|
8
9
|
export function surfaceCovers(entry, surface) {
|
|
9
|
-
|
|
10
|
+
const key = templateSurface(surface);
|
|
11
|
+
return key === entry || key.startsWith(`${entry}.`);
|
|
10
12
|
}
|
|
11
13
|
/**
|
|
12
14
|
* Whether an editor may insert this component on `surface`: it is not marked
|
package/dist/definitions.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* data — the validator exists so a theme's test suite fails loudly on catalog
|
|
5
5
|
* bugs when the catalog is assembled.
|
|
6
6
|
*/
|
|
7
|
-
import { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, guaranteedRole, innerGuarantees, placementAllows, surfaceRules, } from "./blocks.js";
|
|
7
|
+
import { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, LANDING_TEMPLATE, guaranteedRole, innerGuarantees, placementAllows, surfaceRules, } from "./blocks.js";
|
|
8
8
|
import { instantiatePreset, surfaceCovers } from "./composition.js";
|
|
9
9
|
import { conditionProblem } from "./conditions.js";
|
|
10
10
|
import { COLOR_SCHEME_ID_PATTERN, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, colorSchemesProblem, styleSizeProblem, styleValueProblem, } from "./style.js";
|
|
@@ -117,7 +117,8 @@ export function validateThemeDefinition(theme, options = {}) {
|
|
|
117
117
|
addIssue(issues, "surfaces", "invalid-theme", "`surfaces` must be an array of surface declarations");
|
|
118
118
|
}
|
|
119
119
|
else {
|
|
120
|
-
|
|
120
|
+
const areaKeys = new Set((Array.isArray(theme.areas) ? theme.areas : []).map((area) => (isRecord(area) ? area.key : undefined)).filter((key) => typeof key === "string"));
|
|
121
|
+
inspectSurfaces(theme.surfaces, issues, locales, new Set(Object.keys(theme.components)), areaKeys);
|
|
121
122
|
}
|
|
122
123
|
}
|
|
123
124
|
if (theme.areas !== undefined)
|
|
@@ -324,7 +325,7 @@ function inspectAreas(areas, issues, locales, catalogKeys) {
|
|
|
324
325
|
inspectBlockBounds(area, path, issues, catalogKeys, THEME_LIMITS.sections, "invalid-theme");
|
|
325
326
|
});
|
|
326
327
|
}
|
|
327
|
-
function inspectSurfaces(surfaces, issues, locales, catalogKeys) {
|
|
328
|
+
function inspectSurfaces(surfaces, issues, locales, catalogKeys, areaKeys) {
|
|
328
329
|
const seenKeys = new Set();
|
|
329
330
|
surfaces.forEach((surface, index) => {
|
|
330
331
|
const surfacePath = `surfaces[${index}]`;
|
|
@@ -349,6 +350,28 @@ function inspectSurfaces(surfaces, issues, locales, catalogKeys) {
|
|
|
349
350
|
if (surface.pageType !== undefined && !PAGE_TYPES.includes(surface.pageType)) {
|
|
350
351
|
addIssue(issues, `${surfacePath}.pageType`, "invalid-theme", `\`pageType\` must be one of ${PAGE_TYPES.join(", ")}`);
|
|
351
352
|
}
|
|
353
|
+
if (typeof surface.key === "string" && (surface.key.startsWith("lp:") || surface.key.startsWith("area:"))) {
|
|
354
|
+
addIssue(issues, `${surfacePath}.key`, "invalid-theme", `"${surface.key}" uses a prefix the platform keeps for a landing page's or an area's own document`);
|
|
355
|
+
}
|
|
356
|
+
if (surface.key === LANDING_TEMPLATE && surface.pageType !== "landing") {
|
|
357
|
+
addIssue(issues, `${surfacePath}.pageType`, "invalid-theme", `the "${LANDING_TEMPLATE}" surface renders landing pages: its \`pageType\` is "landing"`);
|
|
358
|
+
}
|
|
359
|
+
if (surface.pageType === "landing" && typeof surface.key === "string" && surface.key !== LANDING_TEMPLATE) {
|
|
360
|
+
addIssue(issues, `${surfacePath}.key`, "invalid-theme", `landing pages are composed by the "${LANDING_TEMPLATE}" surface; each page's own document follows it`);
|
|
361
|
+
}
|
|
362
|
+
if (surface.hideableAreas !== undefined) {
|
|
363
|
+
const listed = surface.hideableAreas;
|
|
364
|
+
if (!Array.isArray(listed) || new Set(listed).size !== listed.length) {
|
|
365
|
+
addIssue(issues, `${surfacePath}.hideableAreas`, "invalid-theme", "`hideableAreas` must be a list of distinct area keys");
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
listed.forEach((key, position) => {
|
|
369
|
+
if (typeof key !== "string" || !areaKeys.has(key)) {
|
|
370
|
+
addIssue(issues, `${surfacePath}.hideableAreas[${position}]`, "invalid-theme", `${JSON.stringify(key)} is not an area this theme declares`);
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
}
|
|
352
375
|
if (surface.document !== undefined && !isRecord(surface.document)) {
|
|
353
376
|
addIssue(issues, `${surfacePath}.document`, "invalid-document", "`document` must be a page document");
|
|
354
377
|
}
|
|
@@ -603,12 +626,12 @@ function inspectBlockRules(theme, issues) {
|
|
|
603
626
|
if (issues.length > 0)
|
|
604
627
|
return;
|
|
605
628
|
(theme.surfaces ?? []).forEach((surface, index) => {
|
|
606
|
-
if (surface.pageType !== "product")
|
|
629
|
+
if (surface.pageType !== "product" && surface.pageType !== "landing")
|
|
607
630
|
return;
|
|
608
631
|
const rules = surfaceRules(theme, surface.key);
|
|
609
632
|
const guaranteed = (rules.required ?? []).reduce((count, entry) => count + (entry.min ?? 1) * guaranteedRole(theme, entry.type, "order-form"), 0);
|
|
610
633
|
if (guaranteed === 0) {
|
|
611
|
-
addIssue(issues, `surfaces[${index}]`, "order-form-not-guaranteed",
|
|
634
|
+
addIssue(issues, `surfaces[${index}]`, "order-form-not-guaranteed", `${surface.pageType} surface "${surface.key}" must always render an order form: require a section that places a component with role "order-form" through fixed or required blocks`);
|
|
612
635
|
}
|
|
613
636
|
});
|
|
614
637
|
}
|
package/dist/image-service.d.ts
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An Astro image service for Wocart storefronts: every `<Image>` and
|
|
3
|
-
* `getImage()` of a
|
|
4
|
-
* on
|
|
3
|
+
* `getImage()` of a store image is resized by the store's own media delivery
|
|
4
|
+
* — ImageKit or Cloudflare — so a storefront on any host gets the same URLs
|
|
5
|
+
* and no second image bill. Any other image (another host, a file imported
|
|
6
|
+
* into the theme) is served as it is, and so is every image of a `direct`
|
|
7
|
+
* delivery, which does not resize.
|
|
5
8
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* imported into the theme — is served as it is. Without a media domain (local
|
|
10
|
-
* development, a store without one) every image passes through unchanged.
|
|
11
|
-
*
|
|
12
|
-
* The domain is read per call, not at build: one theme build serves every
|
|
13
|
-
* store. Bind it once, in the theme, to wherever the store's media domain
|
|
14
|
-
* lives:
|
|
9
|
+
* The delivery is the store config's `media`, read per image, not at build:
|
|
10
|
+
* one theme build serves every store. Bind it once, in the theme, to where
|
|
11
|
+
* the theme keeps the store's config:
|
|
15
12
|
*
|
|
16
13
|
* ```ts
|
|
17
14
|
* // src/image-service.ts
|
|
18
|
-
* import { MEDIA_DOMAIN } from "astro:env/server";
|
|
19
15
|
* import { mediaImageService } from "@wocart/theme/image-service";
|
|
20
|
-
*
|
|
16
|
+
* import { storeMedia } from "./store-media";
|
|
17
|
+
* export default mediaImageService(storeMedia);
|
|
21
18
|
* ```
|
|
22
19
|
*
|
|
23
20
|
* ```js
|
|
@@ -27,13 +24,13 @@
|
|
|
27
24
|
* ```
|
|
28
25
|
*/
|
|
29
26
|
import type { ExternalImageService, ImageTransform } from "astro";
|
|
30
|
-
import { type MediaImageParams } from "@wocart/storefront";
|
|
31
|
-
/** The
|
|
27
|
+
import { type MediaDelivery, type MediaImageParams } from "@wocart/storefront";
|
|
28
|
+
/** The resizing options an Astro image transform asks for. */
|
|
32
29
|
export declare function mediaImageParams(options: ImageTransform): MediaImageParams;
|
|
33
30
|
/**
|
|
34
|
-
* The image service, bound to the store's media
|
|
31
|
+
* The image service, bound to the store's media delivery.
|
|
35
32
|
*
|
|
36
|
-
* @param
|
|
37
|
-
* store's
|
|
33
|
+
* @param delivery - Read on every image (e.g. `() => config.media`): the
|
|
34
|
+
* store's `MediaDelivery`, or `null` / `undefined` to pass every image through.
|
|
38
35
|
*/
|
|
39
|
-
export declare function mediaImageService(
|
|
36
|
+
export declare function mediaImageService(delivery: () => MediaDelivery | null | undefined | Promise<MediaDelivery | null | undefined>): ExternalImageService;
|
package/dist/image-service.js
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* An Astro image service for Wocart storefronts: every `<Image>` and
|
|
3
|
-
* `getImage()` of a
|
|
4
|
-
* on
|
|
3
|
+
* `getImage()` of a store image is resized by the store's own media delivery
|
|
4
|
+
* — ImageKit or Cloudflare — so a storefront on any host gets the same URLs
|
|
5
|
+
* and no second image bill. Any other image (another host, a file imported
|
|
6
|
+
* into the theme) is served as it is, and so is every image of a `direct`
|
|
7
|
+
* delivery, which does not resize.
|
|
5
8
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* imported into the theme — is served as it is. Without a media domain (local
|
|
10
|
-
* development, a store without one) every image passes through unchanged.
|
|
11
|
-
*
|
|
12
|
-
* The domain is read per call, not at build: one theme build serves every
|
|
13
|
-
* store. Bind it once, in the theme, to wherever the store's media domain
|
|
14
|
-
* lives:
|
|
9
|
+
* The delivery is the store config's `media`, read per image, not at build:
|
|
10
|
+
* one theme build serves every store. Bind it once, in the theme, to where
|
|
11
|
+
* the theme keeps the store's config:
|
|
15
12
|
*
|
|
16
13
|
* ```ts
|
|
17
14
|
* // src/image-service.ts
|
|
18
|
-
* import { MEDIA_DOMAIN } from "astro:env/server";
|
|
19
15
|
* import { mediaImageService } from "@wocart/theme/image-service";
|
|
20
|
-
*
|
|
16
|
+
* import { storeMedia } from "./store-media";
|
|
17
|
+
* export default mediaImageService(storeMedia);
|
|
21
18
|
* ```
|
|
22
19
|
*
|
|
23
20
|
* ```js
|
|
@@ -28,8 +25,8 @@
|
|
|
28
25
|
*/
|
|
29
26
|
import { baseService } from "astro/assets";
|
|
30
27
|
import { mediaImageUrl } from "@wocart/storefront";
|
|
31
|
-
/** Astro's `fit` (CSS object-fit) in
|
|
32
|
-
const
|
|
28
|
+
/** Astro's `fit` (CSS object-fit) in the words `mediaImageUrl` takes; a value it also knows passes as it is. */
|
|
29
|
+
const FIT = {
|
|
33
30
|
cover: "cover",
|
|
34
31
|
contain: "contain",
|
|
35
32
|
fill: "squeeze",
|
|
@@ -38,7 +35,7 @@ const CLOUDFLARE_FIT = {
|
|
|
38
35
|
pad: "pad",
|
|
39
36
|
squeeze: "squeeze",
|
|
40
37
|
};
|
|
41
|
-
/** The
|
|
38
|
+
/** The resizing options an Astro image transform asks for. */
|
|
42
39
|
export function mediaImageParams(options) {
|
|
43
40
|
const params = {};
|
|
44
41
|
if (options.width)
|
|
@@ -49,25 +46,25 @@ export function mediaImageParams(options) {
|
|
|
49
46
|
params.format = options.format;
|
|
50
47
|
if (typeof options.quality === "number")
|
|
51
48
|
params.quality = options.quality;
|
|
52
|
-
const fit = options.fit ?
|
|
49
|
+
const fit = options.fit ? FIT[options.fit] : undefined;
|
|
53
50
|
if (fit)
|
|
54
51
|
params.fit = fit;
|
|
55
52
|
return params;
|
|
56
53
|
}
|
|
57
54
|
/**
|
|
58
|
-
* The image service, bound to the store's media
|
|
55
|
+
* The image service, bound to the store's media delivery.
|
|
59
56
|
*
|
|
60
|
-
* @param
|
|
61
|
-
* store's
|
|
57
|
+
* @param delivery - Read on every image (e.g. `() => config.media`): the
|
|
58
|
+
* store's `MediaDelivery`, or `null` / `undefined` to pass every image through.
|
|
62
59
|
*/
|
|
63
|
-
export function mediaImageService(
|
|
60
|
+
export function mediaImageService(delivery) {
|
|
64
61
|
return {
|
|
65
62
|
...baseService,
|
|
66
|
-
getURL(options) {
|
|
63
|
+
async getURL(options) {
|
|
67
64
|
const src = options.src;
|
|
68
65
|
if (typeof src !== "string")
|
|
69
66
|
return src.src;
|
|
70
|
-
return mediaImageUrl(src, mediaImageParams(options),
|
|
67
|
+
return mediaImageUrl(src, mediaImageParams(options), await delivery()) ?? src;
|
|
71
68
|
},
|
|
72
69
|
};
|
|
73
70
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -17,12 +17,12 @@ export { resolveInstance, createInstance, checkStoredValue as checkFieldValue }
|
|
|
17
17
|
export type { CheckedValue as FieldValueCheck } from "./instances.js";
|
|
18
18
|
export { RICH_TEXT_FORMATS, richTextToHtml, richTextToPlainText } from "./rich-text.js";
|
|
19
19
|
export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
20
|
-
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, fixedBlockId } from "./blocks.js";
|
|
20
|
+
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, LANDING_TEMPLATE, THEME_LIMITS, areaSurface, fixedBlockId, hideableAreas, isAreaShown, landingSurface, templateSurface, } from "./blocks.js";
|
|
21
21
|
export { completePage, resolveSurfaceDocument } from "./surface-document.js";
|
|
22
22
|
export { CONDITION_DEPTH, isFieldVisible } from "./conditions.js";
|
|
23
23
|
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, SCHEME_MARKER_CLASS, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
24
24
|
export type { SurfaceDocument, SurfaceDocumentReason } from "./surface-document.js";
|
|
25
|
-
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
25
|
+
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setAreaHidden, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
26
26
|
export type { DocumentEdit, DocumentIssue, DocumentIssueCode, InstanceLocation, InstancePlacement, } from "./operations.js";
|
|
27
27
|
export { THEME_SDK_VERSION } from "./version.js";
|
|
28
28
|
export type { RichTextHtmlOptions } from "./rich-text.js";
|
package/dist/index.js
CHANGED
|
@@ -16,11 +16,11 @@ export { parsePageData, validatePageData } from "./page-data.js";
|
|
|
16
16
|
export { resolveInstance, createInstance, checkStoredValue as checkFieldValue } from "./instances.js";
|
|
17
17
|
export { RICH_TEXT_FORMATS, richTextToHtml, richTextToPlainText } from "./rich-text.js";
|
|
18
18
|
export { instantiatePreset, isInsertableOn } from "./composition.js";
|
|
19
|
-
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, THEME_LIMITS, areaSurface, fixedBlockId } from "./blocks.js";
|
|
19
|
+
export { COMPONENT_PLACEMENTS, COMPONENT_ROLES, LANDING_TEMPLATE, THEME_LIMITS, areaSurface, fixedBlockId, hideableAreas, isAreaShown, landingSurface, templateSurface, } from "./blocks.js";
|
|
20
20
|
export { completePage, resolveSurfaceDocument } from "./surface-document.js";
|
|
21
21
|
export { CONDITION_DEPTH, isFieldVisible } from "./conditions.js";
|
|
22
22
|
export { COLOR_ROLES, COLOR_SCHEMES_MAX, STYLE_BREAKPOINTS, STYLE_PANELS, STYLE_STEPS, STYLE_WIDTHS, THEME_STYLES_PATH, SCHEME_MARKER_CLASS, colorSchemeCss, contrastRatio, schemeClass, styleClasses, themeStyleSheet, } from "./style.js";
|
|
23
|
-
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
23
|
+
export { checkPageData, duplicateInstance, findInstance, insertInstance, moveInstance, newInstanceId, removeInstance, setAreaHidden, setInstanceHidden, setInstanceProps, } from "./operations.js";
|
|
24
24
|
export { THEME_SDK_VERSION } from "./version.js";
|
|
25
25
|
export { resolveThemeSettings, checkThemeSettings } from "./settings.js";
|
|
26
26
|
export { localize } from "./locales.js";
|
package/dist/manifest.d.ts
CHANGED
|
@@ -21,8 +21,8 @@ export declare const THEME_MANIFEST_PATH = "/wocart-theme.json";
|
|
|
21
21
|
/** Derive the data-only manifest from a theme. `component` and `load` are dropped. */
|
|
22
22
|
export declare function themeManifest(theme: ThemeDefinition): ThemeManifest;
|
|
23
23
|
/**
|
|
24
|
-
* The manifest's structural shape. Unknown keys are stripped
|
|
25
|
-
*
|
|
24
|
+
* The manifest's structural shape. Unknown keys are stripped, so a manifest
|
|
25
|
+
* from a newer theme still reads; absent `fields`/`slots` normalize to `[]`.
|
|
26
26
|
* Rules that span fields are not here — `validateThemeManifest` applies them.
|
|
27
27
|
*/
|
|
28
28
|
export declare const ThemeManifestSchema: z.ZodObject<{
|
|
@@ -41,6 +41,7 @@ export declare const ThemeManifestSchema: z.ZodObject<{
|
|
|
41
41
|
max: z.ZodExactOptional<z.ZodNumber>;
|
|
42
42
|
}, z.core.$strip>>>;
|
|
43
43
|
max: z.ZodExactOptional<z.ZodNumber>;
|
|
44
|
+
hideableAreas: z.ZodExactOptional<z.ZodArray<z.ZodString>>;
|
|
44
45
|
}, z.core.$strip>>>;
|
|
45
46
|
areas: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
46
47
|
key: z.ZodString;
|
package/dist/manifest.js
CHANGED
|
@@ -37,6 +37,7 @@ export function themeManifest(theme) {
|
|
|
37
37
|
...(surface.document === undefined ? {} : { document: copyData(surface.document) }),
|
|
38
38
|
...(surface.required === undefined ? {} : { required: copyData(surface.required) }),
|
|
39
39
|
...(surface.max === undefined ? {} : { max: surface.max }),
|
|
40
|
+
...(surface.hideableAreas === undefined ? {} : { hideableAreas: [...surface.hideableAreas] }),
|
|
40
41
|
})),
|
|
41
42
|
areas: (theme.areas ?? []).map((area) => copyData(area)),
|
|
42
43
|
locales: [...(theme.locales ?? [])],
|
|
@@ -214,8 +215,8 @@ const ComponentSpecSchema = z.object({
|
|
|
214
215
|
.default([]),
|
|
215
216
|
});
|
|
216
217
|
/**
|
|
217
|
-
* The manifest's structural shape. Unknown keys are stripped
|
|
218
|
-
*
|
|
218
|
+
* The manifest's structural shape. Unknown keys are stripped, so a manifest
|
|
219
|
+
* from a newer theme still reads; absent `fields`/`slots` normalize to `[]`.
|
|
219
220
|
* Rules that span fields are not here — `validateThemeManifest` applies them.
|
|
220
221
|
*/
|
|
221
222
|
export const ThemeManifestSchema = z.object({
|
|
@@ -232,6 +233,7 @@ export const ThemeManifestSchema = z.object({
|
|
|
232
233
|
document: z.exactOptional(z.custom((value) => isRecord(value))),
|
|
233
234
|
required: z.exactOptional(z.array(RequiredBlockSchema)),
|
|
234
235
|
max: z.exactOptional(z.number()),
|
|
236
|
+
hideableAreas: z.exactOptional(z.array(z.string())),
|
|
235
237
|
}))
|
|
236
238
|
.default([]),
|
|
237
239
|
areas: z
|
package/dist/operations.d.ts
CHANGED
|
@@ -87,6 +87,11 @@ export declare function setInstanceProps(theme: ThemeDefinition, page: PageData,
|
|
|
87
87
|
export declare function setInstanceHidden(theme: ThemeDefinition, page: PageData, id: string, hidden: boolean): DocumentEdit;
|
|
88
88
|
/** @deprecated Pass the theme first, so required blocks stay shown. */
|
|
89
89
|
export declare function setInstanceHidden(page: PageData, id: string, hidden: boolean): DocumentEdit;
|
|
90
|
+
/**
|
|
91
|
+
* Show or leave out one of the theme's areas on this page — the header, the
|
|
92
|
+
* footer — when its surface lets a page leave it out (`hideableAreas`).
|
|
93
|
+
*/
|
|
94
|
+
export declare function setAreaHidden(theme: ThemeDefinition, page: PageData, key: string, hidden: boolean): DocumentEdit;
|
|
90
95
|
/**
|
|
91
96
|
* Everything a writer must fix before saving a document for this theme —
|
|
92
97
|
* the strict counterpart of `validatePageData`: an envelope for one of the
|
package/dist/operations.js
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* nesting stay within their caps. Operations on a document assume it was
|
|
15
15
|
* completed (`completePage`) when it was opened.
|
|
16
16
|
*/
|
|
17
|
-
import { THEME_LIMITS, composesSurface, placementAllows, slotCap, surfaceRules } from "./blocks.js";
|
|
17
|
+
import { THEME_LIMITS, composesSurface, hideableAreas, placementAllows, slotCap, surfaceRules } from "./blocks.js";
|
|
18
18
|
import { surfaceCovers } from "./composition.js";
|
|
19
19
|
import { checkStoredValue, hasFallback, ownValue } from "./instances.js";
|
|
20
20
|
import { ComponentInstanceSchema } from "./schemas.js";
|
|
@@ -518,6 +518,24 @@ export function setInstanceHidden(...args) {
|
|
|
518
518
|
delete location.instance.hidden;
|
|
519
519
|
return { ok: true, page: next };
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Show or leave out one of the theme's areas on this page — the header, the
|
|
523
|
+
* footer — when its surface lets a page leave it out (`hideableAreas`).
|
|
524
|
+
*/
|
|
525
|
+
export function setAreaHidden(theme, page, key, hidden) {
|
|
526
|
+
if (!hideableAreas(theme, page.surface).includes(key)) {
|
|
527
|
+
return refuse("not-allowed", `a page of "${page.surface}" cannot leave out the area "${key}"`);
|
|
528
|
+
}
|
|
529
|
+
const next = structuredClone(page);
|
|
530
|
+
const rest = (next.hiddenAreas ?? []).filter((candidate) => candidate !== key);
|
|
531
|
+
const order = (theme.areas ?? []).map((area) => area.key);
|
|
532
|
+
const hiddenAreas = hidden ? [...rest, key].sort((a, b) => order.indexOf(a) - order.indexOf(b)) : rest;
|
|
533
|
+
if (hiddenAreas.length > 0)
|
|
534
|
+
next.hiddenAreas = hiddenAreas;
|
|
535
|
+
else
|
|
536
|
+
delete next.hiddenAreas;
|
|
537
|
+
return { ok: true, page: next };
|
|
538
|
+
}
|
|
521
539
|
/**
|
|
522
540
|
* Everything a writer must fix before saving a document for this theme —
|
|
523
541
|
* the strict counterpart of `validatePageData`: an envelope for one of the
|
|
@@ -538,6 +556,13 @@ export function checkPageData(theme, input) {
|
|
|
538
556
|
if (input.theme !== undefined && !(isRecord(input.theme) && input.theme.key === theme.key && typeof input.theme.version === "string" && input.theme.version.length > 0)) {
|
|
539
557
|
issues.push({ path: "theme", code: "invalid-document", message: `\`theme\` must name this theme: { key: "${theme.key}", version }` });
|
|
540
558
|
}
|
|
559
|
+
if (input.hiddenAreas !== undefined) {
|
|
560
|
+
const hideable = hideableAreas(theme, surface);
|
|
561
|
+
const hidden = input.hiddenAreas;
|
|
562
|
+
if (!Array.isArray(hidden) || new Set(hidden).size !== hidden.length || !hidden.every((key) => typeof key === "string" && hideable.includes(key))) {
|
|
563
|
+
issues.push({ path: "hiddenAreas", code: "invalid-document", message: `\`hiddenAreas\` lists distinct areas this page may leave out: ${hideable.length > 0 ? hideable.join(", ") : "none"}` });
|
|
564
|
+
}
|
|
565
|
+
}
|
|
541
566
|
const rules = surfaceRules(theme, surface);
|
|
542
567
|
const cap = slotCap(rules, THEME_LIMITS.sections);
|
|
543
568
|
if (input.sections.length > cap) {
|
package/dist/page-data.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
* failed; a structurally invalid section is dropped with an issue; the
|
|
8
8
|
* document itself is not rewritten by `validatePageData`.
|
|
9
9
|
*/
|
|
10
|
-
import { THEME_LIMITS, completeRequired, slotCap, surfaceRules } from "./blocks.js";
|
|
10
|
+
import { THEME_LIMITS, completeRequired, hideableAreas, slotCap, surfaceRules } from "./blocks.js";
|
|
11
11
|
import { resolveInstance } from "./instances.js";
|
|
12
|
-
import { ComponentInstanceSchema, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN } from "./schemas.js";
|
|
12
|
+
import { ComponentInstanceSchema, SURFACE_KEY_PATTERN, THEME_KEY_PATTERN, TYPE_KEY_PATTERN } from "./schemas.js";
|
|
13
13
|
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
14
|
+
/** At most this many areas a document leaves out: a theme declares a handful. */
|
|
15
|
+
const MAX_HIDDEN_AREAS = 8;
|
|
14
16
|
const envelopeFailure = (message) => ({
|
|
15
17
|
page: null,
|
|
16
18
|
issues: [{ path: "", code: "envelope-invalid", message }],
|
|
@@ -53,12 +55,25 @@ export function parsePageData(input) {
|
|
|
53
55
|
// name a prototype.
|
|
54
56
|
const page = { version: 1, surface, sections };
|
|
55
57
|
for (const key of Object.keys(input)) {
|
|
56
|
-
if (key === "version" || key === "surface" || key === "sections" || key === "theme")
|
|
58
|
+
if (key === "version" || key === "surface" || key === "sections" || key === "theme" || key === "hiddenAreas")
|
|
57
59
|
continue;
|
|
58
60
|
if (key === "__proto__")
|
|
59
61
|
continue;
|
|
60
62
|
page[key] = input[key];
|
|
61
63
|
}
|
|
64
|
+
if (input.hiddenAreas !== undefined) {
|
|
65
|
+
const hidden = input.hiddenAreas;
|
|
66
|
+
if (Array.isArray(hidden) && hidden.length <= MAX_HIDDEN_AREAS && hidden.every((key) => typeof key === "string" && TYPE_KEY_PATTERN.test(key)) && new Set(hidden).size === hidden.length) {
|
|
67
|
+
page.hiddenAreas = [...hidden];
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
issues.push({
|
|
71
|
+
path: "hiddenAreas",
|
|
72
|
+
code: "invalid-prop",
|
|
73
|
+
message: `\`hiddenAreas\` must list at most ${MAX_HIDDEN_AREAS} distinct area keys; ignored, every area shows`,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
62
77
|
if (input.theme !== undefined) {
|
|
63
78
|
const theme = input.theme;
|
|
64
79
|
if (isRecord(theme) &&
|
|
@@ -124,6 +139,12 @@ export function validatePageData(theme, input, scope = {}) {
|
|
|
124
139
|
walk(block, `${path}.fixed.${name}`);
|
|
125
140
|
}
|
|
126
141
|
};
|
|
142
|
+
const hideable = hideableAreas(theme, parsed.page.surface);
|
|
143
|
+
(parsed.page.hiddenAreas ?? []).forEach((key, index) => {
|
|
144
|
+
if (!hideable.includes(key)) {
|
|
145
|
+
issues.push({ path: `hiddenAreas[${index}]`, code: "invalid-prop", message: `"${key}" is not an area this page may leave out; it shows` });
|
|
146
|
+
}
|
|
147
|
+
});
|
|
127
148
|
const spec = surfaceRules(theme, parsed.page.surface);
|
|
128
149
|
const cap = slotCap(spec, THEME_LIMITS.sections);
|
|
129
150
|
if (parsed.page.sections.length > cap) {
|
package/dist/preview-target.d.ts
CHANGED
|
@@ -29,6 +29,9 @@ export interface PreviewTarget {
|
|
|
29
29
|
* when the theme composes that page, else on the home page.
|
|
30
30
|
* - The resource is chosen by the surface's `pageType`, never its key, so an
|
|
31
31
|
* alternate (`product.compact`) previews like its base.
|
|
32
|
+
* - A landing page's own document (`lp:<id>`) previews with the theme's
|
|
33
|
+
* `landing` surface on the page its `path` names (`/lp/<slug>`), and the
|
|
34
|
+
* target keeps its own surface, so the sections render that page's document.
|
|
32
35
|
*
|
|
33
36
|
* ```astro
|
|
34
37
|
* ---
|
package/dist/preview-target.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* document it renders.
|
|
5
5
|
*/
|
|
6
6
|
import { DEFAULT_COLLECTION_HANDLES, matchStorefrontPath } from "@wocart/storefront";
|
|
7
|
-
import { areaSurface } from "./blocks.js";
|
|
7
|
+
import { areaSurface, templateSurface } from "./blocks.js";
|
|
8
8
|
/** The surface a page type is shown with: the one keyed by the type itself, else the first declaring it. */
|
|
9
9
|
function surfaceFor(surfaces, pageType) {
|
|
10
10
|
return surfaces.find((surface) => surface.key === pageType) ?? surfaces.find((surface) => surface.pageType === pageType);
|
|
@@ -45,6 +45,9 @@ async function resourceParams(pageType, named, client) {
|
|
|
45
45
|
* when the theme composes that page, else on the home page.
|
|
46
46
|
* - The resource is chosen by the surface's `pageType`, never its key, so an
|
|
47
47
|
* alternate (`product.compact`) previews like its base.
|
|
48
|
+
* - A landing page's own document (`lp:<id>`) previews with the theme's
|
|
49
|
+
* `landing` surface on the page its `path` names (`/lp/<slug>`), and the
|
|
50
|
+
* target keeps its own surface, so the sections render that page's document.
|
|
48
51
|
*
|
|
49
52
|
* ```astro
|
|
50
53
|
* ---
|
|
@@ -58,14 +61,16 @@ export async function previewTarget(theme, draft, client) {
|
|
|
58
61
|
const surfaces = theme.surfaces ?? [];
|
|
59
62
|
const match = draft.path ? matchStorefrontPath(draft.path) : null;
|
|
60
63
|
const area = (theme.areas ?? []).some((candidate) => areaSurface(candidate.key) === draft.surface) ? draft.surface : null;
|
|
61
|
-
const
|
|
64
|
+
const template = templateSurface(draft.surface);
|
|
65
|
+
const shown = area === null ? surfaces.find((surface) => surface.key === template) : (match ? surfaceFor(surfaces, match.type) : undefined) ?? surfaceFor(surfaces, "home");
|
|
62
66
|
if (shown === undefined)
|
|
63
67
|
return null;
|
|
68
|
+
const surface = area === null ? draft.surface : shown.key;
|
|
64
69
|
const pageType = shown.pageType;
|
|
65
70
|
const named = match !== null && match.type === pageType ? { ...match.params } : {};
|
|
66
71
|
const [params, document] = await Promise.all([
|
|
67
72
|
resourceParams(pageType, named, client),
|
|
68
|
-
area === null && draft.document !== undefined ? draft.document : client.pageData.get(
|
|
73
|
+
area === null && draft.document !== undefined ? draft.document : client.pageData.get(surface),
|
|
69
74
|
]);
|
|
70
|
-
return { surface
|
|
75
|
+
return { surface, pageType, area, params, document };
|
|
71
76
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -728,6 +728,13 @@ export interface SurfaceSpec {
|
|
|
728
728
|
required?: readonly RequiredBlock[];
|
|
729
729
|
/** At most this many top-level sections (1–50). Absent: `THEME_LIMITS.sections`. */
|
|
730
730
|
max?: number;
|
|
731
|
+
/**
|
|
732
|
+
* The theme's areas a page of this surface may leave out (`["header",
|
|
733
|
+
* "footer"]`): its document lists the ones it hides in `hiddenAreas`. A
|
|
734
|
+
* landing page that sends an ad's visitor straight to its order form is
|
|
735
|
+
* the case. Absent: every page shows every area.
|
|
736
|
+
*/
|
|
737
|
+
hideableAreas?: readonly string[];
|
|
731
738
|
}
|
|
732
739
|
/**
|
|
733
740
|
* A part every page shares — a header, a footer — composed of sections like a
|
|
@@ -868,6 +875,11 @@ export interface PageData {
|
|
|
868
875
|
version: 1;
|
|
869
876
|
surface: string;
|
|
870
877
|
sections: ComponentInstance[];
|
|
878
|
+
/**
|
|
879
|
+
* The keys of the theme's areas this page leaves out. Only the areas its
|
|
880
|
+
* surface lists in `hideableAreas` are left out; read it with `isAreaShown`.
|
|
881
|
+
*/
|
|
882
|
+
hiddenAreas?: string[];
|
|
871
883
|
/**
|
|
872
884
|
* The theme and theme version that wrote the document. A document that
|
|
873
885
|
* names another theme is reported (`theme-mismatch`): its sections were
|
package/dist/version.d.ts
CHANGED
package/dist/version.js
CHANGED