platform-types 1.10.688 → 1.10.689

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -79,6 +79,8 @@ export * from './storefrontTheme.ts';
79
79
  export * from './storeNetwork.ts';
80
80
  export * from './storefrontAssets.ts';
81
81
  export * from './storefrontEvent.ts';
82
+ export * from './storefrontSeo.ts';
83
+ export * from './storefrontWebLiteralKeys.ts';
82
84
  export * from './subscription.ts';
83
85
  export * from './support.ts';
84
86
  export * from './terminal.ts';
package/dist/index.js CHANGED
@@ -87,6 +87,8 @@ export * from "./storefrontTheme.js";
87
87
  export * from "./storeNetwork.js";
88
88
  export * from "./storefrontAssets.js";
89
89
  export * from "./storefrontEvent.js";
90
+ export * from "./storefrontSeo.js";
91
+ export * from "./storefrontWebLiteralKeys.js";
90
92
  export * from "./subscription.js";
91
93
  export * from "./support.js";
92
94
  export * from "./terminal.js";
@@ -1,4 +1,13 @@
1
1
  import type { StorefrontThemeWarning } from './storefrontTheme.ts';
2
+ /** The social kinds a contact may link, each to its own hosts; a contact links at most one per kind. */
3
+ export declare const STOREFRONT_SOCIAL_HOSTS: {
4
+ readonly instagram: readonly ["instagram.com", "www.instagram.com"];
5
+ readonly facebook: readonly ["facebook.com", "www.facebook.com"];
6
+ readonly youtube: readonly ["youtube.com", "www.youtube.com", "youtu.be", "www.youtu.be"];
7
+ readonly tiktok: readonly ["tiktok.com", "www.tiktok.com"];
8
+ readonly linkedin: readonly ["linkedin.com", "www.linkedin.com"];
9
+ readonly x: readonly ["x.com", "www.x.com"];
10
+ };
2
11
  export declare const STOREFRONT_DESIGN_LIMITS: {
3
12
  readonly customPages: 20;
4
13
  readonly pageAliases: 20;
@@ -37,9 +46,17 @@ export declare const STOREFRONT_DESIGN_LIMITS: {
37
46
  readonly contactAddress: 500;
38
47
  readonly contactNote: 200;
39
48
  readonly contactHours: 14;
40
- readonly contactSocials: 8;
49
+ /** One link per social kind, so the cap is the number of kinds. */
50
+ readonly contactSocials: number;
41
51
  readonly contactEmail: 254;
42
52
  };
53
+ /**
54
+ * A custom page's slug: lowercase ASCII letters, digits and inner hyphens, 1 to
55
+ * `STOREFRONT_DESIGN_LIMITS.slug` characters, starting and ending alphanumeric. The one
56
+ * definition: the api's validators, its page renderer and the edge viewer request read it.
57
+ */
58
+ export declare const STOREFRONT_PAGE_SLUG_RE: RegExp;
59
+ export declare const isStorefrontPageSlug: (value: unknown) => value is string;
43
60
  export declare const STOREFRONT_SECTION_TYPES: readonly ["banner", "imageText", "products", "categories", "brands", "text", "faq", "benefits", "contact", "paymentDelivery"];
44
61
  export declare const STOREFRONT_BUILTIN_DESTINATIONS: readonly ["home", "catalogue", "offers", "new", "search", "cart", "account", "privacy"];
45
62
  /**
@@ -55,14 +72,6 @@ export declare const STOREFRONT_PROTECTED_BUILTIN_DESTINATIONS: readonly ["searc
55
72
  export declare const STOREFRONT_CONTACT_FIELDS: readonly ["email", "phone", "whatsapp", "address", "hours", "socials"];
56
73
  export declare const STOREFRONT_RESERVED_SLUGS: readonly ["home", "index", "api", "auth", "login", "logout", "account", "cart", "checkout", "orders", "search", "products", "product", "producto", "coleccion", "categories", "brands", "sale", "new", "privacy", "terms", "sitemap", "robots", "manifest", "assets", "preview", "admin", "storefront", "pagina", "basket", "profile", "favorites", "invoices", "payments", "payment-methods", "user", "mantenimiento"];
57
74
  export declare const STOREFRONT_BENEFIT_ICONS: readonly ["truck", "shield", "credit-card", "store", "heart", "check"];
58
- export declare const STOREFRONT_SOCIAL_HOSTS: {
59
- readonly instagram: readonly ["instagram.com", "www.instagram.com"];
60
- readonly facebook: readonly ["facebook.com", "www.facebook.com"];
61
- readonly youtube: readonly ["youtube.com", "www.youtube.com", "youtu.be", "www.youtu.be"];
62
- readonly tiktok: readonly ["tiktok.com", "www.tiktok.com"];
63
- readonly linkedin: readonly ["linkedin.com", "www.linkedin.com"];
64
- readonly x: readonly ["x.com", "www.x.com"];
65
- };
66
75
  export declare function createStorefrontDesignV1(): StorefrontDesignV1;
67
76
  declare global {
68
77
  type StorefrontDesignSectionType = (typeof STOREFRONT_SECTION_TYPES)[number];
@@ -272,7 +281,7 @@ declare global {
272
281
  };
273
282
  /**
274
283
  * Image placements gain `url`/`mobileUrl`: the variant-LESS base
275
- * `/sf-media/{stage}/{storeId}/{publicationId}/{assetId}`, a path on the images host. Append
284
+ * `/media/{stage}/{storeId}/{publicationId}/{assetId}`, a path on the images host. Append
276
285
  * `/${StorefrontAssetVariant}` (one of `STOREFRONT_ASSET_VARIANTS`) to request it; the base
277
286
  * alone is not servable. Never route it through `/fit-in/`: the media path is refused there.
278
287
  * Private preview envelopes instead supply `/preview/read?operation=asset&assetId=...`; the
@@ -400,6 +409,17 @@ declare global {
400
409
  items: StorefrontDesignHistoryEntry[];
401
410
  cursor?: string;
402
411
  }
412
+ /** A page of the live publication: the slug it answers at and the former slugs that 301 to it. */
413
+ interface StorefrontDesignPublishedPage {
414
+ slug: string;
415
+ aliases: string[];
416
+ }
417
+ /** Where a resolved setting's value came from: the registry default, a platform layer, or the document. */
418
+ type StorefrontDesignSettingProvenance = 'default' | 'platform' | 'design';
419
+ /**
420
+ * GET draft and every mutation that answers the draft (init, save, discard, restore) carry the
421
+ * same result; an idempotent replay rebuilds the four derived fields from current reads.
422
+ */
403
423
  interface StorefrontDesignDraftResult {
404
424
  draftRevision: string;
405
425
  publishedRevision: string | null;
@@ -407,6 +427,28 @@ declare global {
407
427
  dirty: boolean;
408
428
  mappingConflicts: StorefrontDesignMappingConflict[];
409
429
  legacySourceRevision: string;
430
+ /**
431
+ * The live publication's page addresses, keyed by page id: every page that has ever been
432
+ * published and still owns its slug, a since-removed or disabled page's included. Renaming
433
+ * an ENABLED page listed here makes its current slug a 301 alias on the next publish; a
434
+ * page absent here has no live address, so renaming it creates no alias. Absent when the
435
+ * shop has never published.
436
+ */
437
+ publishedPages?: Record<string, StorefrontDesignPublishedPage>;
438
+ /**
439
+ * What publishing this saved document would refuse today, as field errors: missing required
440
+ * content, a link to a disabled or missing page (`DANGLING_REFERENCE`), a slug a published
441
+ * page still owns (`ALIAS_CONFLICT`), a page out of aliases (`ALIAS_LIMIT`), and one
442
+ * `MAPPING_CONFLICT` per unresolved `mappingConflicts` entry, at that entry's path (the
443
+ * publish refusal itself names the single path `mappingConflicts`). Empty when publishable.
444
+ * Never blocks a save, and never covers asset readiness, which publish probes on its own
445
+ * (`STOREFRONT_ASSET_NOT_READY`).
446
+ */
447
+ publishIssues?: StorefrontDesignFieldError[];
448
+ /** The document's settings over the live platform layer: what the published shop would render. */
449
+ effectiveSettings?: StorefrontDesignSettings;
450
+ /** Per setting, which layer `effectiveSettings` took it from. */
451
+ settingsProvenance?: Record<keyof StorefrontDesignSettings, StorefrontDesignSettingProvenance>;
410
452
  }
411
453
  interface StorefrontDesignDeliveryTarget {
412
454
  /**
@@ -516,6 +558,27 @@ declare global {
516
558
  assetId: string;
517
559
  variant?: StorefrontAssetVariant;
518
560
  };
561
+ /**
562
+ * The codes `GET /preview/read` answers itself, on the common `{ message, error }` body:
563
+ * 400 `STOREFRONT_PREVIEW_INVALID` (a query the read schema refuses, a repeated parameter or a
564
+ * body), 401 `STOREFRONT_PREVIEW_INVALID` (no or a malformed bearer grant), 401 `…_EXPIRED`,
565
+ * 403 `…_REVOKED` / `…_FORBIDDEN` / `PLAN_UPGRADE_REQUIRED` (bare, without the editor
566
+ * routes' plan fields), 404 `ASSET_NOT_FOUND` (an asset the pinned draft does not reference or
567
+ * that is not ready), 405 `METHOD_NOT_ALLOWED` (anything but GET and OPTIONS), 413
568
+ * `STOREFRONT_PREVIEW_IMAGE_TOO_LARGE` (asset bytes over 4 MiB) and 503 `…_UNAVAILABLE`.
569
+ */
570
+ type StorefrontPreviewReadErrorCode = 'STOREFRONT_PREVIEW_INVALID' | 'STOREFRONT_PREVIEW_EXPIRED' | 'STOREFRONT_PREVIEW_REVOKED' | 'STOREFRONT_PREVIEW_FORBIDDEN' | 'STOREFRONT_PREVIEW_UNAVAILABLE' | 'STOREFRONT_PREVIEW_IMAGE_TOO_LARGE' | 'PLAN_UPGRADE_REQUIRED' | 'ASSET_NOT_FOUND' | 'METHOD_NOT_ALLOWED';
571
+ /**
572
+ * The codes a catalogue operation answers through its public web route's own core, as that
573
+ * route does: 404 `PRODUCT_NOT_FOUND` / `PRODUCT_GROUP_NOT_FOUND` / `COLLECTION_NOT_FOUND`,
574
+ * 400 `INVALID_CURSOR` / `INVALID_DISCOVERY_QUERY`, 409 `CURSOR_STALE` (a collection that
575
+ * changed between the reads bracketing a page) and 503 `PRODUCT_LOOKUP_INCOMPLETE`.
576
+ */
577
+ type StorefrontPreviewCatalogueErrorCode = 'PRODUCT_NOT_FOUND' | 'PRODUCT_GROUP_NOT_FOUND' | 'COLLECTION_NOT_FOUND' | 'INVALID_CURSOR' | 'INVALID_DISCOVERY_QUERY' | 'CURSOR_STALE' | 'PRODUCT_LOOKUP_INCOMPLETE';
578
+ interface StorefrontPreviewReadFailure {
579
+ message: string;
580
+ error: StorefrontPreviewReadErrorCode | StorefrontPreviewCatalogueErrorCode;
581
+ }
519
582
  interface StorefrontPreviewDesignResult {
520
583
  mode: 'preview';
521
584
  draftRevision: string;
@@ -1,3 +1,12 @@
1
+ /** The social kinds a contact may link, each to its own hosts; a contact links at most one per kind. */
2
+ export const STOREFRONT_SOCIAL_HOSTS = {
3
+ instagram: ['instagram.com', 'www.instagram.com'],
4
+ facebook: ['facebook.com', 'www.facebook.com'],
5
+ youtube: ['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be'],
6
+ tiktok: ['tiktok.com', 'www.tiktok.com'],
7
+ linkedin: ['linkedin.com', 'www.linkedin.com'],
8
+ x: ['x.com', 'www.x.com'],
9
+ };
1
10
  export const STOREFRONT_DESIGN_LIMITS = {
2
11
  customPages: 20,
3
12
  pageAliases: 20,
@@ -36,9 +45,17 @@ export const STOREFRONT_DESIGN_LIMITS = {
36
45
  contactAddress: 500,
37
46
  contactNote: 200,
38
47
  contactHours: 14,
39
- contactSocials: 8,
48
+ /** One link per social kind, so the cap is the number of kinds. */
49
+ contactSocials: Object.keys(STOREFRONT_SOCIAL_HOSTS).length,
40
50
  contactEmail: 254,
41
51
  };
52
+ /**
53
+ * A custom page's slug: lowercase ASCII letters, digits and inner hyphens, 1 to
54
+ * `STOREFRONT_DESIGN_LIMITS.slug` characters, starting and ending alphanumeric. The one
55
+ * definition: the api's validators, its page renderer and the edge viewer request read it.
56
+ */
57
+ export const STOREFRONT_PAGE_SLUG_RE = /^[a-z0-9](?:[a-z0-9-]{0,78}[a-z0-9])?$/;
58
+ export const isStorefrontPageSlug = (value) => typeof value === 'string' && STOREFRONT_PAGE_SLUG_RE.test(value);
42
59
  export const STOREFRONT_SECTION_TYPES = [
43
60
  'banner',
44
61
  'imageText',
@@ -117,14 +134,6 @@ export const STOREFRONT_RESERVED_SLUGS = [
117
134
  'mantenimiento',
118
135
  ];
119
136
  export const STOREFRONT_BENEFIT_ICONS = ['truck', 'shield', 'credit-card', 'store', 'heart', 'check'];
120
- export const STOREFRONT_SOCIAL_HOSTS = {
121
- instagram: ['instagram.com', 'www.instagram.com'],
122
- facebook: ['facebook.com', 'www.facebook.com'],
123
- youtube: ['youtube.com', 'www.youtube.com', 'youtu.be', 'www.youtu.be'],
124
- tiktok: ['tiktok.com', 'www.tiktok.com'],
125
- linkedin: ['linkedin.com', 'www.linkedin.com'],
126
- x: ['x.com', 'www.x.com'],
127
- };
128
137
  export function createStorefrontDesignV1() {
129
138
  return {
130
139
  schemaVersion: 1,
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Information-page paths and the storefront's SEO inheritance chain — one
3
+ * definition for the server that renders a shared link's head and the editor
4
+ * that previews it.
5
+ */
6
+ /** Path prefix of a published information page: `/pagina/<slug>`. */
7
+ export declare const STOREFRONT_PAGE_PATH_PREFIX = "/pagina/";
8
+ /** The path an information page with `slug` is served at. The slug is neither validated nor encoded here. */
9
+ export declare const storefrontPagePath: (slug: string) => string;
10
+ /** The description a shop is shared with when neither the page nor the shop sets one. */
11
+ export declare const storefrontDefaultSeoDescription: (storeName: string) => string;
12
+ /**
13
+ * Where a resolved SEO field came from: the page itself, the shop-wide `seo`
14
+ * block, the shop logo (image only), or the platform default.
15
+ */
16
+ export type StorefrontSeoSource = 'page' | 'shop' | 'logo' | 'default';
17
+ /** The SEO fields a page and the shop each carry; `Placement` is the image shape of the document passed in. */
18
+ export interface StorefrontSeoFields<Placement = StorefrontDesignImagePlacement> {
19
+ title?: string;
20
+ description?: string;
21
+ image?: Placement;
22
+ noindex?: boolean;
23
+ }
24
+ /**
25
+ * What {@link resolveStorefrontSeo} reads from a design document. A draft
26
+ * (`StorefrontDesignV1`), the published projection and `PublicStorefrontDesign`
27
+ * all satisfy it; with the last, a resolved image keeps its `url`.
28
+ */
29
+ export interface StorefrontSeoDocument<Placement = StorefrontDesignImagePlacement> {
30
+ seo: StorefrontSeoFields<Placement>;
31
+ branding: {
32
+ logo?: Placement;
33
+ };
34
+ pages: readonly {
35
+ id: string;
36
+ title: string;
37
+ seo?: StorefrontSeoFields<Placement>;
38
+ }[];
39
+ }
40
+ /** The SEO a page is shared with, each field with where it came from. */
41
+ export interface ResolvedStorefrontSeo<Placement = StorefrontDesignImagePlacement> {
42
+ title: string;
43
+ description: string;
44
+ /**
45
+ * The placement shared as `og:image` (cropped to 1200x630), or `null` for
46
+ * the platform's own share image (`source.image === 'default'`).
47
+ */
48
+ image: Placement | null;
49
+ noindex: boolean;
50
+ source: {
51
+ title: Exclude<StorefrontSeoSource, 'logo'>;
52
+ description: Exclude<StorefrontSeoSource, 'logo'>;
53
+ image: StorefrontSeoSource;
54
+ noindex: Exclude<StorefrontSeoSource, 'logo'>;
55
+ };
56
+ }
57
+ /**
58
+ * The SEO the storefront shares a page with: `'home'`, or an information page
59
+ * by `id`.
60
+ *
61
+ * - title: the page's `seo.title`, else its `title`, else the shop's
62
+ * `seo.title`, else `storeName`. Home skips the page steps — its own `title`
63
+ * is never used.
64
+ * - description: the page's `seo.description`, else the shop's, else
65
+ * {@link storefrontDefaultSeoDescription}.
66
+ * - image: the page's `seo.image`, else the shop's, else the logo, else `null`
67
+ * (the platform share image).
68
+ * - noindex: the page's `seo.noindex` when set — an explicit `false` beats a
69
+ * shop-wide `true` — else the shop's, else `false`.
70
+ *
71
+ * An empty title or description counts as unset and falls through. Returns
72
+ * `undefined` for a page id the document does not carry; `'home'` always
73
+ * resolves. A shop with no published design is out of scope: its head uses
74
+ * the store's own name, photo and the default description.
75
+ */
76
+ export declare function resolveStorefrontSeo<Placement>(document: StorefrontSeoDocument<Placement>, target: 'home', storeName: string): ResolvedStorefrontSeo<Placement>;
77
+ export declare function resolveStorefrontSeo<Placement>(document: StorefrontSeoDocument<Placement>, target: string, storeName: string): ResolvedStorefrontSeo<Placement> | undefined;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Information-page paths and the storefront's SEO inheritance chain — one
3
+ * definition for the server that renders a shared link's head and the editor
4
+ * that previews it.
5
+ */
6
+ /** Path prefix of a published information page: `/pagina/<slug>`. */
7
+ export const STOREFRONT_PAGE_PATH_PREFIX = '/pagina/';
8
+ /** The path an information page with `slug` is served at. The slug is neither validated nor encoded here. */
9
+ export const storefrontPagePath = (slug) => `${STOREFRONT_PAGE_PATH_PREFIX}${slug}`;
10
+ /** The description a shop is shared with when neither the page nor the shop sets one. */
11
+ export const storefrontDefaultSeoDescription = (storeName) => `Comprá online en ${storeName}: catálogo, precios y stock actualizados.`;
12
+ const shopImage = (document) => {
13
+ if (document.seo.image)
14
+ return { image: document.seo.image, source: 'shop' };
15
+ if (document.branding.logo)
16
+ return { image: document.branding.logo, source: 'logo' };
17
+ return { image: null, source: 'default' };
18
+ };
19
+ /** Home's SEO — and the default every information page inherits field by field. */
20
+ const shopSeo = (document, storeName) => {
21
+ const { seo } = document;
22
+ const image = shopImage(document);
23
+ return {
24
+ title: seo.title || storeName,
25
+ description: seo.description || storefrontDefaultSeoDescription(storeName),
26
+ image: image.image,
27
+ noindex: seo.noindex === true,
28
+ source: {
29
+ title: seo.title ? 'shop' : 'default',
30
+ description: seo.description ? 'shop' : 'default',
31
+ image: image.source,
32
+ noindex: typeof seo.noindex === 'boolean' ? 'shop' : 'default',
33
+ },
34
+ };
35
+ };
36
+ export function resolveStorefrontSeo(document, target, storeName) {
37
+ const shop = shopSeo(document, storeName);
38
+ if (target === 'home')
39
+ return shop;
40
+ const page = document.pages.find(({ id }) => id === target);
41
+ if (!page)
42
+ return undefined;
43
+ const own = page.seo;
44
+ const pageTitle = own?.title || page.title;
45
+ const pageNoindex = own?.noindex;
46
+ return {
47
+ title: pageTitle || shop.title,
48
+ description: own?.description || shop.description,
49
+ image: own?.image || shop.image,
50
+ noindex: typeof pageNoindex === 'boolean' ? pageNoindex : shop.noindex,
51
+ source: {
52
+ title: pageTitle ? 'page' : shop.source.title,
53
+ description: own?.description ? 'page' : shop.source.description,
54
+ image: own?.image ? 'page' : shop.source.image,
55
+ noindex: typeof pageNoindex === 'boolean' ? 'page' : shop.source.noindex,
56
+ },
57
+ };
58
+ }
@@ -1,4 +1,4 @@
1
- import { type StorefrontDesignSettings } from './storefrontDesignRegistry.ts';
1
+ import { type StorefrontDesignSettings, type StorefrontDesignSettingsLayer } from './storefrontDesignRegistry.ts';
2
2
  type StorefrontThemeMode = 'light' | 'dark';
3
3
  type StorefrontThemeSurfaceId = 'page' | 'header' | 'footer';
4
4
  export interface StorefrontThemeSurface {
@@ -51,11 +51,27 @@ export interface StorefrontResolvedTheme {
51
51
  }
52
52
  export declare const storefrontContrastRatio: (foreground: string, background: string) => number;
53
53
  /**
54
- * The storefront theme for a design's settings overrides.
55
- *
56
- * Settings resolve from the registry defaults plus the store's own design
57
- * overrides ONLY — no GLOBAL or WEB layer takes part — so a consumer uses this
58
- * as shipped rather than merging layers of its own first.
54
+ * The GLOBAL design-settings layer of the platform globals row as `getPlatformGlobalsRow`
55
+ * returns it (`{ [key]: { value, ... } }`): each entry's `value`, kept only when it is a
56
+ * valid design setting, so a non-design global or a malformed value never reaches a theme.
59
57
  */
60
- export declare const resolveStorefrontTheme: (overrides?: Partial<StorefrontDesignSettings>) => StorefrontResolvedTheme;
58
+ export declare const storefrontGlobalSettingsLayer: (globalsRow?: Record<string, unknown>) => StorefrontDesignSettingsLayer;
59
+ /**
60
+ * The settings a storefront is served with: registry defaults, then the platform GLOBAL
61
+ * layer, then the design's own overrides. The one resolver behind the public envelope's
62
+ * `effectiveSettings`, the draft's `effectiveSettings` and the publish contrast report —
63
+ * a caller passes the globals row it read, never a layer stack of its own. The edge
64
+ * renderer's `theme-color` is design-only: its role reads no GLOBALS row.
65
+ */
66
+ export declare const resolveStorefrontEffectiveSettings: (overrides?: Partial<StorefrontDesignSettings>, globalsRow?: Record<string, unknown>) => StorefrontDesignSettings;
67
+ /**
68
+ * The storefront theme for a design's settings overrides, resolved through
69
+ * `resolveStorefrontEffectiveSettings`: the GLOBAL layer of `globalsRow` sits under the
70
+ * design, exactly as in the public envelope's `effectiveSettings`. Omitting `globalsRow`
71
+ * resolves defaults plus the design only — correct solely while no GLOBAL design setting
72
+ * is set, so a caller serving or reporting on a storefront passes the row (the edge
73
+ * renderer's `theme-color` is the one exception, design-only by its read grant). Passing
74
+ * already-effective settings as `overrides` rebuilds the same theme.
75
+ */
76
+ export declare const resolveStorefrontTheme: (overrides?: Partial<StorefrontDesignSettings>, globalsRow?: Record<string, unknown>) => StorefrontResolvedTheme;
61
77
  export {};
@@ -1,4 +1,4 @@
1
- import { resolveStorefrontDesignSettings } from "./storefrontDesignRegistry.js";
1
+ import { resolveStorefrontDesignSettings, validateStorefrontDesignSetting, } from "./storefrontDesignRegistry.js";
2
2
  const channels = (color) => [
3
3
  Number.parseInt(color.slice(1, 3), 16),
4
4
  Number.parseInt(color.slice(3, 5), 16),
@@ -62,14 +62,36 @@ const surfaceTokens = (background, settings, mode, surface, warnings) => {
62
62
  };
63
63
  };
64
64
  /**
65
- * The storefront theme for a design's settings overrides.
66
- *
67
- * Settings resolve from the registry defaults plus the store's own design
68
- * overrides ONLY — no GLOBAL or WEB layer takes part — so a consumer uses this
69
- * as shipped rather than merging layers of its own first.
65
+ * The GLOBAL design-settings layer of the platform globals row as `getPlatformGlobalsRow`
66
+ * returns it (`{ [key]: { value, ... } }`): each entry's `value`, kept only when it is a
67
+ * valid design setting, so a non-design global or a malformed value never reaches a theme.
70
68
  */
71
- export const resolveStorefrontTheme = (overrides = {}) => {
72
- const settings = resolveStorefrontDesignSettings(overrides);
69
+ export const storefrontGlobalSettingsLayer = (globalsRow) => ({
70
+ layer: 'GLOBAL',
71
+ values: Object.fromEntries(Object.entries(globalsRow ?? {}).flatMap(([key, entry]) => {
72
+ const value = entry !== null && typeof entry === 'object' ? entry.value : undefined;
73
+ return validateStorefrontDesignSetting(key, value) ? [[key, value]] : [];
74
+ })),
75
+ });
76
+ /**
77
+ * The settings a storefront is served with: registry defaults, then the platform GLOBAL
78
+ * layer, then the design's own overrides. The one resolver behind the public envelope's
79
+ * `effectiveSettings`, the draft's `effectiveSettings` and the publish contrast report —
80
+ * a caller passes the globals row it read, never a layer stack of its own. The edge
81
+ * renderer's `theme-color` is design-only: its role reads no GLOBALS row.
82
+ */
83
+ export const resolveStorefrontEffectiveSettings = (overrides = {}, globalsRow) => resolveStorefrontDesignSettings(overrides, [storefrontGlobalSettingsLayer(globalsRow)]);
84
+ /**
85
+ * The storefront theme for a design's settings overrides, resolved through
86
+ * `resolveStorefrontEffectiveSettings`: the GLOBAL layer of `globalsRow` sits under the
87
+ * design, exactly as in the public envelope's `effectiveSettings`. Omitting `globalsRow`
88
+ * resolves defaults plus the design only — correct solely while no GLOBAL design setting
89
+ * is set, so a caller serving or reporting on a storefront passes the row (the edge
90
+ * renderer's `theme-color` is the one exception, design-only by its read grant). Passing
91
+ * already-effective settings as `overrides` rebuilds the same theme.
92
+ */
93
+ export const resolveStorefrontTheme = (overrides = {}, globalsRow) => {
94
+ const settings = resolveStorefrontEffectiveSettings(overrides, globalsRow);
73
95
  const warnings = [];
74
96
  const makeMode = (mode) => {
75
97
  const background = (requested) => mode === 'light' ? requested.toUpperCase() : mix(requested, '#000000', 0.9);
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Every storefront `WEB` literal key a shop may override, in code-unit order.
3
+ *
4
+ * This list is what the api's override validator accepts — one definition, so
5
+ * an editor offers exactly the keys a save will keep. A key outside it is
6
+ * refused as `UNKNOWN_KEY` on save and dropped on read.
7
+ *
8
+ * ⚠️ It is NOT every key the storefront renders. Withheld on purpose:
9
+ * - every key starting with `PRIVACY_` — privacy and terms are platform text,
10
+ * the same on every shop, and the storefront ignores a shop value for them;
11
+ * - the keys only the not-found and unavailable screens render, which read no
12
+ * shop layer;
13
+ * - keys whose storefront value is not a plain `{name}`-token sentence (it
14
+ * branches, computes, or is not a string), which a shop string cannot
15
+ * express.
16
+ * An editor that lists any of those offers an edit the server refuses.
17
+ *
18
+ * Interpolated values use `{name}` tokens named after the storefront's own
19
+ * parameters.
20
+ */
21
+ export declare const STOREFRONT_WEB_LITERAL_KEYS: readonly string[];
22
+ /** Whether a shop may override `key` — membership in {@link STOREFRONT_WEB_LITERAL_KEYS}. */
23
+ export declare const isStorefrontLiteralKey: (key: string) => boolean;