@zalify/storefront-kit 0.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.
Files changed (213) hide show
  1. package/LICENSE.md +49 -0
  2. package/README.md +63 -0
  3. package/dist/commerce/countdown.d.ts +18 -0
  4. package/dist/commerce/countdown.js +28 -0
  5. package/dist/commerce/events.d.ts +29 -0
  6. package/dist/commerce/events.js +14 -0
  7. package/dist/commerce/facets.d.ts +27 -0
  8. package/dist/commerce/facets.js +71 -0
  9. package/dist/commerce/index.d.ts +11 -0
  10. package/dist/commerce/index.js +11 -0
  11. package/dist/commerce/money.d.ts +51 -0
  12. package/dist/commerce/money.js +83 -0
  13. package/dist/commerce/product.d.ts +102 -0
  14. package/dist/commerce/product.js +93 -0
  15. package/dist/editor/frame.d.ts +57 -0
  16. package/dist/editor/frame.js +241 -0
  17. package/dist/editor/host.d.ts +31 -0
  18. package/dist/editor/host.js +67 -0
  19. package/dist/editor/index.d.ts +9 -0
  20. package/dist/editor/index.js +9 -0
  21. package/dist/react/adapter.d.ts +49 -0
  22. package/dist/react/adapter.js +23 -0
  23. package/dist/react/blocks/_box.d.ts +10 -0
  24. package/dist/react/blocks/_box.js +17 -0
  25. package/dist/react/blocks/_slide.d.ts +24 -0
  26. package/dist/react/blocks/_slide.js +21 -0
  27. package/dist/react/blocks/_video-card.d.ts +11 -0
  28. package/dist/react/blocks/_video-card.js +39 -0
  29. package/dist/react/blocks/accordion.d.ts +12 -0
  30. package/dist/react/blocks/accordion.js +5 -0
  31. package/dist/react/blocks/button.d.ts +10 -0
  32. package/dist/react/blocks/button.js +15 -0
  33. package/dist/react/blocks/countdown.d.ts +9 -0
  34. package/dist/react/blocks/countdown.js +64 -0
  35. package/dist/react/blocks/custom-liquid.d.ts +11 -0
  36. package/dist/react/blocks/custom-liquid.js +4 -0
  37. package/dist/react/blocks/group.d.ts +9 -0
  38. package/dist/react/blocks/group.js +8 -0
  39. package/dist/react/blocks/image.d.ts +8 -0
  40. package/dist/react/blocks/image.js +7 -0
  41. package/dist/react/blocks/message.d.ts +14 -0
  42. package/dist/react/blocks/message.js +5 -0
  43. package/dist/react/blocks/quote.d.ts +9 -0
  44. package/dist/react/blocks/quote.js +5 -0
  45. package/dist/react/blocks/spacer.d.ts +8 -0
  46. package/dist/react/blocks/spacer.js +5 -0
  47. package/dist/react/blocks/story.d.ts +17 -0
  48. package/dist/react/blocks/story.js +7 -0
  49. package/dist/react/blocks/text.d.ts +9 -0
  50. package/dist/react/blocks/text.js +5 -0
  51. package/dist/react/blocks/video.d.ts +17 -0
  52. package/dist/react/blocks/video.js +56 -0
  53. package/dist/react/components/Facets.d.ts +21 -0
  54. package/dist/react/components/Facets.js +220 -0
  55. package/dist/react/components/Icon.d.ts +12 -0
  56. package/dist/react/components/Icon.js +66 -0
  57. package/dist/react/components/PaymentIcons.d.ts +5 -0
  58. package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
  59. package/dist/react/components/PaymentIcons.generated.js +21 -0
  60. package/dist/react/components/PaymentIcons.js +18 -0
  61. package/dist/react/components/Price.d.ts +24 -0
  62. package/dist/react/components/Price.js +9 -0
  63. package/dist/react/components/ProductBadges.d.ts +24 -0
  64. package/dist/react/components/ProductBadges.js +57 -0
  65. package/dist/react/components/ProductCard.d.ts +17 -0
  66. package/dist/react/components/ProductCard.js +202 -0
  67. package/dist/react/components/Rating.d.ts +11 -0
  68. package/dist/react/components/Rating.js +35 -0
  69. package/dist/react/components/SwatchStyle.d.ts +23 -0
  70. package/dist/react/components/SwatchStyle.js +22 -0
  71. package/dist/react/components/VideoModal.d.ts +55 -0
  72. package/dist/react/components/VideoModal.js +102 -0
  73. package/dist/react/engine/CssVariables.d.ts +1 -0
  74. package/dist/react/engine/CssVariables.js +105 -0
  75. package/dist/react/engine/context.d.ts +43 -0
  76. package/dist/react/engine/context.js +45 -0
  77. package/dist/react/engine/fonts.d.ts +15 -0
  78. package/dist/react/engine/fonts.js +33 -0
  79. package/dist/react/engine/images.d.ts +24 -0
  80. package/dist/react/engine/images.js +40 -0
  81. package/dist/react/engine/render.d.ts +29 -0
  82. package/dist/react/engine/render.js +87 -0
  83. package/dist/react/engine/settings.d.ts +8 -0
  84. package/dist/react/engine/settings.js +68 -0
  85. package/dist/react/engine/store.d.ts +64 -0
  86. package/dist/react/engine/store.js +42 -0
  87. package/dist/react/engine/translate.d.ts +7 -0
  88. package/dist/react/engine/translate.js +33 -0
  89. package/dist/react/engine/types.d.ts +128 -0
  90. package/dist/react/engine/types.js +1 -0
  91. package/dist/react/index.d.ts +38 -0
  92. package/dist/react/index.js +39 -0
  93. package/dist/react/registries.d.ts +10 -0
  94. package/dist/react/registries.js +68 -0
  95. package/dist/react/registries.server.d.ts +2 -0
  96. package/dist/react/registries.server.js +98 -0
  97. package/dist/react/sections/404.d.ts +10 -0
  98. package/dist/react/sections/404.js +10 -0
  99. package/dist/react/sections/announcement-bar.d.ts +14 -0
  100. package/dist/react/sections/announcement-bar.js +84 -0
  101. package/dist/react/sections/article.d.ts +17 -0
  102. package/dist/react/sections/article.js +31 -0
  103. package/dist/react/sections/contact-form.d.ts +16 -0
  104. package/dist/react/sections/contact-form.js +8 -0
  105. package/dist/react/sections/custom-liquid.d.ts +14 -0
  106. package/dist/react/sections/custom-liquid.js +7 -0
  107. package/dist/react/sections/custom-section.d.ts +14 -0
  108. package/dist/react/sections/custom-section.js +9 -0
  109. package/dist/react/sections/featured-collection.d.ts +10 -0
  110. package/dist/react/sections/featured-collection.js +23 -0
  111. package/dist/react/sections/hero.d.ts +6 -0
  112. package/dist/react/sections/hero.js +35 -0
  113. package/dist/react/sections/image-with-text.d.ts +13 -0
  114. package/dist/react/sections/image-with-text.js +9 -0
  115. package/dist/react/sections/multicolumn.d.ts +15 -0
  116. package/dist/react/sections/multicolumn.js +8 -0
  117. package/dist/react/sections/page.d.ts +13 -0
  118. package/dist/react/sections/page.js +11 -0
  119. package/dist/react/sections/password.d.ts +15 -0
  120. package/dist/react/sections/password.js +9 -0
  121. package/dist/react/sections/rich-text.d.ts +12 -0
  122. package/dist/react/sections/rich-text.js +7 -0
  123. package/dist/react/sections/stories.d.ts +18 -0
  124. package/dist/react/sections/stories.js +10 -0
  125. package/dist/react/sections/video-bubble.d.ts +13 -0
  126. package/dist/react/sections/video-bubble.js +54 -0
  127. package/dist/react/sections/video-carousel.d.ts +19 -0
  128. package/dist/react/sections/video-carousel.js +9 -0
  129. package/dist/react/server.d.ts +7 -0
  130. package/dist/react/server.js +42 -0
  131. package/dist/schemas/bridge.d.ts +98 -0
  132. package/dist/schemas/bridge.js +50 -0
  133. package/dist/schemas/data.d.ts +54 -0
  134. package/dist/schemas/data.js +22 -0
  135. package/dist/schemas/index.d.ts +14 -0
  136. package/dist/schemas/index.js +14 -0
  137. package/dist/schemas/manifest.d.ts +63 -0
  138. package/dist/schemas/manifest.js +32 -0
  139. package/dist/schemas/schema.d.ts +159 -0
  140. package/dist/schemas/schema.js +129 -0
  141. package/dist/schemas/validate.d.ts +22 -0
  142. package/dist/schemas/validate.js +199 -0
  143. package/package.json +84 -0
  144. package/src/commerce/countdown.ts +38 -0
  145. package/src/commerce/events.ts +43 -0
  146. package/src/commerce/facets.ts +88 -0
  147. package/src/commerce/index.ts +11 -0
  148. package/src/commerce/money.ts +120 -0
  149. package/src/commerce/product.ts +177 -0
  150. package/src/editor/frame.ts +305 -0
  151. package/src/editor/host.ts +101 -0
  152. package/src/editor/index.ts +9 -0
  153. package/src/react/adapter.tsx +83 -0
  154. package/src/react/blocks/_box.tsx +37 -0
  155. package/src/react/blocks/_slide.tsx +96 -0
  156. package/src/react/blocks/_video-card.tsx +131 -0
  157. package/src/react/blocks/accordion.tsx +33 -0
  158. package/src/react/blocks/button.tsx +36 -0
  159. package/src/react/blocks/countdown.tsx +105 -0
  160. package/src/react/blocks/custom-liquid.tsx +21 -0
  161. package/src/react/blocks/group.tsx +32 -0
  162. package/src/react/blocks/image.tsx +29 -0
  163. package/src/react/blocks/message.tsx +28 -0
  164. package/src/react/blocks/quote.tsx +24 -0
  165. package/src/react/blocks/spacer.tsx +17 -0
  166. package/src/react/blocks/story.tsx +40 -0
  167. package/src/react/blocks/text.tsx +24 -0
  168. package/src/react/blocks/video.tsx +132 -0
  169. package/src/react/components/Facets.tsx +424 -0
  170. package/src/react/components/Icon.tsx +96 -0
  171. package/src/react/components/PaymentIcons.generated.ts +24 -0
  172. package/src/react/components/PaymentIcons.tsx +27 -0
  173. package/src/react/components/Price.tsx +72 -0
  174. package/src/react/components/ProductBadges.tsx +95 -0
  175. package/src/react/components/ProductCard.tsx +417 -0
  176. package/src/react/components/Rating.tsx +59 -0
  177. package/src/react/components/SwatchStyle.tsx +50 -0
  178. package/src/react/components/VideoModal.tsx +236 -0
  179. package/src/react/engine/CssVariables.tsx +142 -0
  180. package/src/react/engine/context.tsx +108 -0
  181. package/src/react/engine/fonts.ts +42 -0
  182. package/src/react/engine/images.tsx +58 -0
  183. package/src/react/engine/render.tsx +190 -0
  184. package/src/react/engine/settings.ts +89 -0
  185. package/src/react/engine/store.ts +103 -0
  186. package/src/react/engine/translate.ts +40 -0
  187. package/src/react/engine/types.ts +142 -0
  188. package/src/react/index.ts +78 -0
  189. package/src/react/registries.server.ts +110 -0
  190. package/src/react/registries.ts +80 -0
  191. package/src/react/sections/404.tsx +37 -0
  192. package/src/react/sections/announcement-bar.tsx +170 -0
  193. package/src/react/sections/article.tsx +97 -0
  194. package/src/react/sections/contact-form.tsx +104 -0
  195. package/src/react/sections/custom-liquid.tsx +39 -0
  196. package/src/react/sections/custom-section.tsx +44 -0
  197. package/src/react/sections/featured-collection.tsx +89 -0
  198. package/src/react/sections/hero.tsx +84 -0
  199. package/src/react/sections/image-with-text.tsx +61 -0
  200. package/src/react/sections/multicolumn.tsx +46 -0
  201. package/src/react/sections/page.tsx +43 -0
  202. package/src/react/sections/password.tsx +60 -0
  203. package/src/react/sections/rich-text.tsx +37 -0
  204. package/src/react/sections/stories.tsx +51 -0
  205. package/src/react/sections/video-bubble.tsx +145 -0
  206. package/src/react/sections/video-carousel.tsx +57 -0
  207. package/src/react/server.ts +60 -0
  208. package/src/schemas/bridge.ts +143 -0
  209. package/src/schemas/data.ts +72 -0
  210. package/src/schemas/index.ts +14 -0
  211. package/src/schemas/manifest.ts +85 -0
  212. package/src/schemas/schema.ts +287 -0
  213. package/src/schemas/validate.ts +302 -0
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Server side of the section engine (import from '@zalify/storefront-kit/react/server').
3
+ * Route loaders run the registered per-section-type loaders for a
4
+ * template and pass the result to <ThemeTemplate sectionData={…}/>.
5
+ */
6
+ import {getSectionGroup, getTemplate, getThemeStore} from './engine/store';
7
+ import type {SectionLoaderArgs, TemplateData} from './engine/types';
8
+
9
+ export {builtinSectionLoaders} from './registries.server';
10
+
11
+ type LoaderContext = Omit<
12
+ SectionLoaderArgs,
13
+ 'settings' | 'section' | 'sectionId'
14
+ >;
15
+
16
+ async function runLoaders(
17
+ template: TemplateData,
18
+ context: LoaderContext,
19
+ ): Promise<Record<string, unknown>> {
20
+ const loaders = getThemeStore().sectionLoaders ?? {};
21
+ const entries = await Promise.all(
22
+ template.order.map(async (sectionId) => {
23
+ const section = template.sections[sectionId];
24
+ if (!section || section.disabled) return null;
25
+ const loader = loaders[section.type];
26
+ if (!loader) return null;
27
+ try {
28
+ const data = await loader({
29
+ ...context,
30
+ settings: section.settings ?? {},
31
+ section,
32
+ sectionId,
33
+ });
34
+ return [sectionId, data] as const;
35
+ } catch (error) {
36
+ // A failing section must not take down the page.
37
+ // eslint-disable-next-line no-console
38
+ console.error(`[theme] loader for section "${sectionId}" failed`, error);
39
+ return null;
40
+ }
41
+ }),
42
+ );
43
+ return Object.fromEntries(entries.filter(Boolean) as [string, unknown][]);
44
+ }
45
+
46
+ /** Run all section loaders for a page template. */
47
+ export async function loadSectionData(
48
+ templateName: string,
49
+ context: LoaderContext,
50
+ ): Promise<Record<string, unknown>> {
51
+ return runLoaders(getTemplate(templateName), context);
52
+ }
53
+
54
+ /** Run all section loaders for a section group (header/footer/overlay). */
55
+ export async function loadGroupSectionData(
56
+ groupName: string,
57
+ context: LoaderContext,
58
+ ): Promise<Record<string, unknown>> {
59
+ return runLoaders(getSectionGroup(groupName), context);
60
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * L3 — Canvas-editor bridge protocol.
3
+ *
4
+ * postMessage contract between the Zalify canvas editor (host) and a
5
+ * theme preview running on another origin inside an iframe (frame).
6
+ * Both sides validate `event.origin` and the message envelope before
7
+ * acting; versions negotiate on CONTRACT_VERSION's major component.
8
+ */
9
+ import type {SectionGroupData, SettingsBag, TemplateData} from './data.ts';
10
+ import type {ThemeEditorManifest} from './manifest.ts';
11
+
12
+ export const BRIDGE_NAMESPACE = 'zalify-editor-bridge';
13
+
14
+ /** Query param that switches a theme preview into editor mode. */
15
+ export const EDITOR_MODE_PARAM = 'zalify-editor';
16
+
17
+ /** DOM attribute carrying a node's theme-JSON path (wire format). */
18
+ export const DATA_PATH_ATTR = 'data-z-path';
19
+ /** DOM attribute the bridge sets on the selected node. */
20
+ export const DATA_SELECTED_ATTR = 'data-z-selected';
21
+
22
+ export type Device = 'desktop' | 'mobile';
23
+
24
+ export interface DOMRectLike {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ }
30
+
31
+ /* ------------------------------ paths ------------------------------ */
32
+
33
+ /** Locates a section or block inside a template or section group. */
34
+ export interface ThemeJsonPath {
35
+ scope: {kind: 'template' | 'group'; name: string};
36
+ sectionKey: string;
37
+ /** Nested block keys, outermost first; empty = the section itself. */
38
+ blockKeys: string[];
39
+ }
40
+
41
+ /**
42
+ * Wire format: "template:index/hero/slide/box", "group:header-group/header".
43
+ * Every segment is URI-component-encoded so names containing "/" stay
44
+ * unambiguous (customer templates are named "customers/login").
45
+ */
46
+ export function formatThemePath(path: ThemeJsonPath): string {
47
+ return [
48
+ `${path.scope.kind}:${encodeURIComponent(path.scope.name)}`,
49
+ encodeURIComponent(path.sectionKey),
50
+ ...path.blockKeys.map(encodeURIComponent),
51
+ ].join('/');
52
+ }
53
+
54
+ export function parseThemePath(wire: string): ThemeJsonPath | null {
55
+ const [scopePart, sectionKey, ...blockKeys] = wire.split('/');
56
+ if (!scopePart || !sectionKey) return null;
57
+ const colon = scopePart.indexOf(':');
58
+ if (colon === -1) return null;
59
+ const kind = scopePart.slice(0, colon);
60
+ const name = scopePart.slice(colon + 1);
61
+ if ((kind !== 'template' && kind !== 'group') || !name) return null;
62
+ return {
63
+ scope: {kind, name: decodeURIComponent(name)},
64
+ sectionKey: decodeURIComponent(sectionKey),
65
+ blockKeys: blockKeys.map(decodeURIComponent),
66
+ };
67
+ }
68
+
69
+ /* ---------------------------- envelope ----------------------------- */
70
+
71
+ export interface BridgeEnvelope<TType extends string, TPayload> {
72
+ z: typeof BRIDGE_NAMESPACE;
73
+ /** CONTRACT_VERSION of the sender. */
74
+ v: string;
75
+ type: TType;
76
+ payload: TPayload;
77
+ }
78
+
79
+ /* ------------------------- host -> frame ---------------------------- */
80
+
81
+ export type HostMessage =
82
+ | BridgeEnvelope<
83
+ 'bridge:init',
84
+ {
85
+ editorOrigin: string;
86
+ device: Device;
87
+ selectedPath: string | null;
88
+ }
89
+ >
90
+ | BridgeEnvelope<'block:select', {path: string | null}>
91
+ | BridgeEnvelope<'block:hover', {path: string | null}>
92
+ | BridgeEnvelope<'block:scroll-to', {path: string}>
93
+ | BridgeEnvelope<
94
+ 'template:apply',
95
+ {
96
+ templateName: string;
97
+ template: TemplateData;
98
+ groups?: Record<string, SectionGroupData>;
99
+ settingsData?: SettingsBag;
100
+ }
101
+ >
102
+ | BridgeEnvelope<'device:set', {device: Device}>
103
+ | BridgeEnvelope<'manifest:request', Record<string, never>>;
104
+
105
+ /* ------------------------- frame -> host ---------------------------- */
106
+
107
+ export type FrameMessage =
108
+ | BridgeEnvelope<
109
+ 'bridge:ready',
110
+ {contractVersion: string; templateName: string; hash: string}
111
+ >
112
+ | BridgeEnvelope<'block:clicked', {path: string; rect: DOMRectLike}>
113
+ | BridgeEnvelope<'block:hovered', {path: string | null; rect?: DOMRectLike}>
114
+ | BridgeEnvelope<
115
+ 'block:rects',
116
+ {rects: Array<{path: string; rect: DOMRectLike}>}
117
+ >
118
+ | BridgeEnvelope<'manifest:response', {manifest: ThemeEditorManifest}>
119
+ | BridgeEnvelope<'template:synced', {hash: string}>
120
+ | BridgeEnvelope<'height:changed', {height: number}>
121
+ | BridgeEnvelope<'navigation', {templateName: string; url: string}>
122
+ | BridgeEnvelope<'bridge:error', {code: string; message: string}>;
123
+
124
+ export type BridgeMessage = HostMessage | FrameMessage;
125
+
126
+ /* ----------------------------- guards ------------------------------- */
127
+
128
+ export function isBridgeMessage(data: unknown): data is BridgeMessage {
129
+ return (
130
+ typeof data === 'object' &&
131
+ data !== null &&
132
+ (data as {z?: unknown}).z === BRIDGE_NAMESPACE &&
133
+ typeof (data as {v?: unknown}).v === 'string' &&
134
+ typeof (data as {type?: unknown}).type === 'string' &&
135
+ typeof (data as {payload?: unknown}).payload === 'object' &&
136
+ (data as {payload?: unknown}).payload !== null
137
+ );
138
+ }
139
+
140
+ /** Same-major = compatible; anything else degrades to read-only preview. */
141
+ export function isCompatibleVersion(mine: string, theirs: string): boolean {
142
+ return mine.split('.')[0] === theirs.split('.')[0];
143
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * L1 — Theme data contract.
3
+ *
4
+ * The runtime JSON shapes of a Zalify theme, byte-compatible with
5
+ * Shopify OS 2.0 theme JSON (templates/*.json, sections/*-group.json,
6
+ * config/settings_data.json). These are the canonical definitions;
7
+ * @zalify/storefront-kit/react's engine types mirror them and will re-export from
8
+ * here as the packages converge.
9
+ */
10
+
11
+ /** A section's or block's settings: setting id -> value. */
12
+ export type SettingsBag = Record<string, unknown>;
13
+
14
+ /** A block instance inside a section (or nested inside another block). */
15
+ export interface BlockData {
16
+ type: string;
17
+ settings?: SettingsBag;
18
+ /** Nested blocks keyed by their stable key. */
19
+ blocks?: Record<string, BlockData>;
20
+ /** Render order of `blocks` keys. */
21
+ block_order?: string[];
22
+ disabled?: boolean;
23
+ }
24
+
25
+ /** A section instance inside a template or section group. */
26
+ export interface SectionData {
27
+ type: string;
28
+ settings?: SettingsBag;
29
+ blocks?: Record<string, BlockData>;
30
+ block_order?: string[];
31
+ disabled?: boolean;
32
+ custom_css?: string[];
33
+ }
34
+
35
+ /** templates/*.json — a page template. */
36
+ export interface TemplateData {
37
+ layout?: string | false;
38
+ wrapper?: string;
39
+ /** Sections keyed by their stable key. */
40
+ sections: Record<string, SectionData>;
41
+ /** Render order of `sections` keys. */
42
+ order: string[];
43
+ }
44
+
45
+ /** sections/*-group.json — header/footer/overlay section groups. */
46
+ export interface SectionGroupData extends TemplateData {
47
+ type: string;
48
+ name: string;
49
+ }
50
+
51
+ /** config/settings_data.json — current values for global theme settings. */
52
+ export interface SettingsData {
53
+ current: string | Record<string, unknown>;
54
+ presets?: Record<string, Record<string, unknown>>;
55
+ }
56
+
57
+ /**
58
+ * Parse theme JSON that may carry Shopify's leading block comment
59
+ * (the theme editor writes templates/*.json with a header comment).
60
+ */
61
+ export function parseThemeJson<T>(raw: string, name = 'theme JSON'): T {
62
+ const stripped = raw.replace(/^\s*\/\*[\s\S]*?\*\//, '').trim();
63
+ try {
64
+ return JSON.parse(stripped) as T;
65
+ } catch (error) {
66
+ throw new Error(
67
+ `[storefront-kit] Failed to parse ${name}: ${
68
+ error instanceof Error ? error.message : String(error)
69
+ }`,
70
+ );
71
+ }
72
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @zalify/storefront-kit/schemas — the Zalify theme contract.
3
+ *
4
+ * L1 (data.ts): runtime theme JSON shapes, = Shopify OS 2.0.
5
+ * L2 (schema.ts): section/block schema types, = `{% schema %}` semantics.
6
+ * (manifest.ts): the generated editor manifest tying L2 together.
7
+ * L3 (bridge.ts): the canvas-editor postMessage protocol.
8
+ * validate.ts: structural + schema-aware validators.
9
+ */
10
+ export * from './data.ts';
11
+ export * from './schema.ts';
12
+ export * from './manifest.ts';
13
+ export * from './bridge.ts';
14
+ export * from './validate.ts';
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The editor manifest: everything the canvas editor needs to drive its
3
+ * Insert/Layers/Config panels without executing theme code. Generated
4
+ * into the theme (and every scaffolded project) by `pnpm sync` as
5
+ * config/editor-manifest.generated.json; served live by the editor
6
+ * bridge as `manifest:response`.
7
+ */
8
+ import type {SectionSchema, SettingDefinition, ThemeBlockSchema} from './schema.ts';
9
+
10
+ /**
11
+ * Contract version spoken by this package. Bumped with the package;
12
+ * editors negotiate compatibility on the major component.
13
+ */
14
+ export const CONTRACT_VERSION = '0.1.0';
15
+
16
+ /** Repo-relative path of the generated manifest inside a theme/project. */
17
+ export const EDITOR_MANIFEST_PATH = 'config/editor-manifest.generated.json';
18
+
19
+ export interface SectionManifestEntry extends SectionSchema {
20
+ /**
21
+ * True when the section has a server-side data loader; settings
22
+ * changes may render stale until the file write round-trips.
23
+ * Enriched at runtime by the bridge (sync.mjs cannot know).
24
+ */
25
+ hasLoader?: boolean;
26
+ }
27
+
28
+ export interface ThemeEditorManifest {
29
+ /** Provenance note ("scripts/sync.mjs — do not edit…"). */
30
+ generatedBy?: string;
31
+ contractVersion: string;
32
+ theme: {name: string; version: string};
33
+ /** Section schemas keyed by section type. */
34
+ sections: Record<string, SectionManifestEntry>;
35
+ /** Theme block schemas keyed by block type (includes "_" private blocks). */
36
+ blocks: Record<string, ThemeBlockSchema>;
37
+ /** = config/settings_schema.json (global theme settings). */
38
+ settingsSchema: Array<{
39
+ name: string;
40
+ theme_name?: string;
41
+ theme_version?: string;
42
+ settings?: SettingDefinition[];
43
+ [key: string]: unknown;
44
+ }>;
45
+ /** Editable template names ("index", "product", "customers/login", …). */
46
+ templates: string[];
47
+ /** Section group names ("header-group", "footer-group", …). */
48
+ sectionGroups: string[];
49
+ /** Flattened "t:" key -> display copy for the editor's locale. */
50
+ locales: Record<string, string>;
51
+ /** Content hash of everything above, for snapshot/bridge consistency. */
52
+ hash: string;
53
+ }
54
+
55
+ /**
56
+ * Resolve a "t:"-prefixed locale key against the manifest's flattened
57
+ * locales; plain strings pass through.
58
+ */
59
+ export function resolveLocale(
60
+ value: string,
61
+ locales: Record<string, string>,
62
+ ): string {
63
+ if (!value.startsWith('t:')) return value;
64
+ return locales[value.slice(2)] ?? value;
65
+ }
66
+
67
+ /**
68
+ * Flatten a nested locale-schema object ({labels: {text: "Text"}}) into
69
+ * "labels.text" -> "Text" pairs, the form `ThemeEditorManifest.locales`
70
+ * carries.
71
+ */
72
+ export function flattenLocales(
73
+ tree: Record<string, unknown>,
74
+ prefix = '',
75
+ out: Record<string, string> = {},
76
+ ): Record<string, string> {
77
+ for (const [key, value] of Object.entries(tree)) {
78
+ const path = prefix ? `${prefix}.${key}` : key;
79
+ if (typeof value === 'string') out[path] = value;
80
+ else if (value && typeof value === 'object') {
81
+ flattenLocales(value as Record<string, unknown>, path, out);
82
+ }
83
+ }
84
+ return out;
85
+ }
@@ -0,0 +1,287 @@
1
+ /**
2
+ * L2 — Theme schema contract.
3
+ *
4
+ * The editor-facing description of sections and blocks: what Shopify
5
+ * carries in a section's `{% schema %}` tag. Field semantics follow
6
+ * Shopify OS 2.0 exactly; this file only gives them an explicit,
7
+ * React-side home (components export these objects, and sync.mjs
8
+ * mirrors them out of the Liquid theme's `{% schema %}` tags).
9
+ */
10
+ import type {BlockData, SectionData, SettingsBag} from './data.ts';
11
+
12
+ /* ------------------------- setting definitions ------------------------- */
13
+
14
+ /**
15
+ * Shopify input setting types, grouped by the tier in which the Zalify
16
+ * canvas editor implements them. Unknown/future types must degrade to a
17
+ * read-only display in the editor — never an error, never value loss.
18
+ */
19
+ export const SETTING_TYPE_TIERS = {
20
+ /** Phase 1 — must render as editable fields. */
21
+ t0: [
22
+ 'text',
23
+ 'textarea',
24
+ 'richtext',
25
+ 'inline_richtext',
26
+ 'number',
27
+ 'range',
28
+ 'checkbox',
29
+ 'select',
30
+ 'radio',
31
+ 'color',
32
+ 'image_picker',
33
+ 'url',
34
+ 'text_alignment',
35
+ ],
36
+ /** Phase 1.5 — theme-level pickers. */
37
+ t1: [
38
+ 'font_picker',
39
+ 'color_background',
40
+ 'color_scheme',
41
+ 'color_scheme_group',
42
+ 'video_url',
43
+ 'html',
44
+ 'link_list',
45
+ ],
46
+ /** Phase 2 — resource pickers backed by storefront data. */
47
+ t2: [
48
+ 'product',
49
+ 'product_list',
50
+ 'collection',
51
+ 'collection_list',
52
+ 'blog',
53
+ 'article',
54
+ 'page',
55
+ 'metaobject',
56
+ 'video',
57
+ ],
58
+ /** Liquid-only; React themes preserve the value but never render it. */
59
+ notApplicable: ['liquid'],
60
+ } as const;
61
+
62
+ export type KnownInputSettingType =
63
+ | (typeof SETTING_TYPE_TIERS.t0)[number]
64
+ | (typeof SETTING_TYPE_TIERS.t1)[number]
65
+ | (typeof SETTING_TYPE_TIERS.t2)[number]
66
+ | (typeof SETTING_TYPE_TIERS.notApplicable)[number];
67
+
68
+ export const SIDEBAR_SETTING_TYPES = ['header', 'paragraph'] as const;
69
+ export type SidebarSettingType = (typeof SIDEBAR_SETTING_TYPES)[number];
70
+
71
+ /** Non-input schema entries (group headers / help copy). */
72
+ export interface SidebarSetting {
73
+ type: SidebarSettingType;
74
+ content: string;
75
+ /** Shopify conditional-visibility expression. */
76
+ visible_if?: string;
77
+ }
78
+
79
+ export interface SettingOption {
80
+ value: string;
81
+ label: string;
82
+ group?: string;
83
+ }
84
+
85
+ /**
86
+ * An input setting definition. One open shape rather than a closed
87
+ * discriminated union: this mirrors the JSON reality (Shopify adds
88
+ * setting types over time) and keeps older editors forward-compatible.
89
+ * Type-specific fields are optional and only meaningful for the types
90
+ * that define them (`options` for select/radio, `min`/`max`/`step`/
91
+ * `unit` for range, `placeholder` for text-likes, …).
92
+ */
93
+ export interface InputSetting {
94
+ type: KnownInputSettingType | (string & {});
95
+ /** Key in the instance's settings bag. */
96
+ id: string;
97
+ /** Supports "t:" locale keys. */
98
+ label: string;
99
+ info?: string;
100
+ default?: unknown;
101
+ visible_if?: string;
102
+ placeholder?: string;
103
+ options?: SettingOption[];
104
+ min?: number;
105
+ max?: number;
106
+ step?: number;
107
+ unit?: string;
108
+ /** range-style sliders on newer setting types. */
109
+ accept?: string[];
110
+ }
111
+
112
+ export type SettingDefinition = InputSetting | SidebarSetting;
113
+
114
+ export function isSidebarSetting(
115
+ setting: SettingDefinition,
116
+ ): setting is SidebarSetting {
117
+ return (SIDEBAR_SETTING_TYPES as readonly string[]).includes(setting.type);
118
+ }
119
+
120
+ export function isInputSetting(
121
+ setting: SettingDefinition,
122
+ ): setting is InputSetting {
123
+ return !isSidebarSetting(setting);
124
+ }
125
+
126
+ /* --------------------------- section schemas --------------------------- */
127
+
128
+ /** Which block types a section (or block) accepts as children. */
129
+ export interface BlockRule {
130
+ /**
131
+ * A concrete block type, or a wildcard: "@theme" (all public theme
132
+ * blocks — types not starting with "_"), "@app" (app blocks).
133
+ */
134
+ type: string;
135
+ limit?: number;
136
+ }
137
+
138
+ /**
139
+ * A section-local block defined inline in the section's schema
140
+ * (Shopify OS 2.0 "section blocks"): the entry carries `name` (and
141
+ * usually `settings`) instead of referencing a theme block. Local
142
+ * blocks cannot nest further blocks.
143
+ */
144
+ export interface LocalBlockDefinition extends BlockRule {
145
+ name: string;
146
+ settings?: SettingDefinition[];
147
+ }
148
+
149
+ /** An entry in a section schema's `blocks` array: reference or inline. */
150
+ export type SectionBlockEntry = BlockRule | LocalBlockDefinition;
151
+
152
+ export function isLocalBlockDefinition(
153
+ entry: SectionBlockEntry,
154
+ ): entry is LocalBlockDefinition {
155
+ return 'name' in entry && typeof entry.name === 'string';
156
+ }
157
+
158
+ export interface PresetBlock {
159
+ type: string;
160
+ settings?: SettingsBag;
161
+ blocks?: PresetBlock[];
162
+ /** Static preset blocks may pin their key (Shopify allows both forms). */
163
+ id?: string;
164
+ }
165
+
166
+ /** An "add section" catalog entry in the editor. */
167
+ export interface SectionPreset {
168
+ name: string;
169
+ settings?: SettingsBag;
170
+ blocks?: PresetBlock[];
171
+ /** Zalify extension: insert-panel grouping. */
172
+ category?: string;
173
+ }
174
+
175
+ export interface TemplateAvailability {
176
+ templates?: string[];
177
+ groups?: string[];
178
+ }
179
+
180
+ /**
181
+ * = Shopify OS 2.0 section schema, minus Liquid-rendering concerns
182
+ * (tag/class/javascript/stylesheet stay in the Liquid theme).
183
+ */
184
+ export interface SectionSchema {
185
+ /** Supports "t:" locale keys. */
186
+ name: string;
187
+ settings?: SettingDefinition[];
188
+ /** Allowed child blocks (references or local definitions); absent = none. */
189
+ blocks?: SectionBlockEntry[];
190
+ /** Defaults to Shopify's cap of 50 when blocks are allowed. */
191
+ max_blocks?: number;
192
+ /** Per-template instance cap (e.g. 1 for header-like sections). */
193
+ limit?: number;
194
+ /** No presets = not manually insertable (main-* sections). */
195
+ presets?: SectionPreset[];
196
+ default?: Pick<SectionData, 'settings' | 'blocks' | 'block_order'>;
197
+ enabled_on?: TemplateAvailability;
198
+ disabled_on?: TemplateAvailability;
199
+ }
200
+
201
+ /** = Shopify OS 2.0 theme block schema (blocks/*.liquid `{% schema %}`). */
202
+ export interface ThemeBlockSchema {
203
+ name: string;
204
+ settings?: SettingDefinition[];
205
+ /** Theme blocks may nest blocks themselves. */
206
+ blocks?: BlockRule[];
207
+ presets?: Array<{
208
+ name: string;
209
+ settings?: SettingsBag;
210
+ blocks?: PresetBlock[];
211
+ }>;
212
+ }
213
+
214
+ /** Shopify's default max_blocks when a section allows blocks. */
215
+ export const DEFAULT_MAX_BLOCKS = 50;
216
+
217
+ /** Block types starting with "_" are private to their section. */
218
+ export function isPrivateBlockType(type: string): boolean {
219
+ return type.startsWith('_');
220
+ }
221
+
222
+ /**
223
+ * Resolve a parent's block rules against the full block registry:
224
+ * which concrete child types may be inserted?
225
+ */
226
+ export function allowedChildTypes(
227
+ rules: SectionBlockEntry[] | undefined,
228
+ allBlockTypes: string[],
229
+ ): string[] {
230
+ if (!rules?.length) return [];
231
+ const allowed = new Set<string>();
232
+ for (const rule of rules) {
233
+ if (rule.type === '@theme') {
234
+ for (const type of allBlockTypes) {
235
+ if (!isPrivateBlockType(type)) allowed.add(type);
236
+ }
237
+ } else if (rule.type !== '@app') {
238
+ allowed.add(rule.type);
239
+ }
240
+ }
241
+ return [...allowed];
242
+ }
243
+
244
+ /** Instantiate a preset's block list into named-map + order form. */
245
+ export function instantiatePresetBlocks(
246
+ presetBlocks: PresetBlock[] | undefined,
247
+ ): Pick<BlockData, 'blocks' | 'block_order'> {
248
+ if (!presetBlocks?.length) return {};
249
+ const blocks: Record<string, BlockData> = {};
250
+ const order: string[] = [];
251
+ const counters = new Map<string, number>();
252
+ for (const preset of presetBlocks) {
253
+ let key = preset.id;
254
+ if (!key || key in blocks) {
255
+ const base = preset.type.replace(/^_/, '');
256
+ const next = (counters.get(base) ?? 0) + 1;
257
+ counters.set(base, next);
258
+ key = `${base}-${next}`;
259
+ while (key in blocks) {
260
+ const bumped = (counters.get(base) ?? 0) + 1;
261
+ counters.set(base, bumped);
262
+ key = `${base}-${bumped}`;
263
+ }
264
+ }
265
+ const nested = instantiatePresetBlocks(preset.blocks);
266
+ blocks[key] = {
267
+ type: preset.type,
268
+ settings: preset.settings ? {...preset.settings} : {},
269
+ ...nested,
270
+ };
271
+ order.push(key);
272
+ }
273
+ return {blocks, block_order: order};
274
+ }
275
+
276
+ /**
277
+ * Generate a stable, non-colliding key for a newly inserted
278
+ * section/block, following Shopify's editor convention
279
+ * (type with private prefix stripped + counter).
280
+ */
281
+ export function generateKey(type: string, siblings: string[]): string {
282
+ const base = type.replace(/^_/, '');
283
+ if (!siblings.includes(base)) return base;
284
+ let index = 1;
285
+ while (siblings.includes(`${base}-${index}`)) index += 1;
286
+ return `${base}-${index}`;
287
+ }