@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.
- package/LICENSE.md +49 -0
- package/README.md +63 -0
- package/dist/commerce/countdown.d.ts +18 -0
- package/dist/commerce/countdown.js +28 -0
- package/dist/commerce/events.d.ts +29 -0
- package/dist/commerce/events.js +14 -0
- package/dist/commerce/facets.d.ts +27 -0
- package/dist/commerce/facets.js +71 -0
- package/dist/commerce/index.d.ts +11 -0
- package/dist/commerce/index.js +11 -0
- package/dist/commerce/money.d.ts +51 -0
- package/dist/commerce/money.js +83 -0
- package/dist/commerce/product.d.ts +102 -0
- package/dist/commerce/product.js +93 -0
- package/dist/editor/frame.d.ts +57 -0
- package/dist/editor/frame.js +241 -0
- package/dist/editor/host.d.ts +31 -0
- package/dist/editor/host.js +67 -0
- package/dist/editor/index.d.ts +9 -0
- package/dist/editor/index.js +9 -0
- package/dist/react/adapter.d.ts +49 -0
- package/dist/react/adapter.js +23 -0
- package/dist/react/blocks/_box.d.ts +10 -0
- package/dist/react/blocks/_box.js +17 -0
- package/dist/react/blocks/_slide.d.ts +24 -0
- package/dist/react/blocks/_slide.js +21 -0
- package/dist/react/blocks/_video-card.d.ts +11 -0
- package/dist/react/blocks/_video-card.js +39 -0
- package/dist/react/blocks/accordion.d.ts +12 -0
- package/dist/react/blocks/accordion.js +5 -0
- package/dist/react/blocks/button.d.ts +10 -0
- package/dist/react/blocks/button.js +15 -0
- package/dist/react/blocks/countdown.d.ts +9 -0
- package/dist/react/blocks/countdown.js +64 -0
- package/dist/react/blocks/custom-liquid.d.ts +11 -0
- package/dist/react/blocks/custom-liquid.js +4 -0
- package/dist/react/blocks/group.d.ts +9 -0
- package/dist/react/blocks/group.js +8 -0
- package/dist/react/blocks/image.d.ts +8 -0
- package/dist/react/blocks/image.js +7 -0
- package/dist/react/blocks/message.d.ts +14 -0
- package/dist/react/blocks/message.js +5 -0
- package/dist/react/blocks/quote.d.ts +9 -0
- package/dist/react/blocks/quote.js +5 -0
- package/dist/react/blocks/spacer.d.ts +8 -0
- package/dist/react/blocks/spacer.js +5 -0
- package/dist/react/blocks/story.d.ts +17 -0
- package/dist/react/blocks/story.js +7 -0
- package/dist/react/blocks/text.d.ts +9 -0
- package/dist/react/blocks/text.js +5 -0
- package/dist/react/blocks/video.d.ts +17 -0
- package/dist/react/blocks/video.js +56 -0
- package/dist/react/components/Facets.d.ts +21 -0
- package/dist/react/components/Facets.js +220 -0
- package/dist/react/components/Icon.d.ts +12 -0
- package/dist/react/components/Icon.js +66 -0
- package/dist/react/components/PaymentIcons.d.ts +5 -0
- package/dist/react/components/PaymentIcons.generated.d.ts +22 -0
- package/dist/react/components/PaymentIcons.generated.js +21 -0
- package/dist/react/components/PaymentIcons.js +18 -0
- package/dist/react/components/Price.d.ts +24 -0
- package/dist/react/components/Price.js +9 -0
- package/dist/react/components/ProductBadges.d.ts +24 -0
- package/dist/react/components/ProductBadges.js +57 -0
- package/dist/react/components/ProductCard.d.ts +17 -0
- package/dist/react/components/ProductCard.js +202 -0
- package/dist/react/components/Rating.d.ts +11 -0
- package/dist/react/components/Rating.js +35 -0
- package/dist/react/components/SwatchStyle.d.ts +23 -0
- package/dist/react/components/SwatchStyle.js +22 -0
- package/dist/react/components/VideoModal.d.ts +55 -0
- package/dist/react/components/VideoModal.js +102 -0
- package/dist/react/engine/CssVariables.d.ts +1 -0
- package/dist/react/engine/CssVariables.js +105 -0
- package/dist/react/engine/context.d.ts +43 -0
- package/dist/react/engine/context.js +45 -0
- package/dist/react/engine/fonts.d.ts +15 -0
- package/dist/react/engine/fonts.js +33 -0
- package/dist/react/engine/images.d.ts +24 -0
- package/dist/react/engine/images.js +40 -0
- package/dist/react/engine/render.d.ts +29 -0
- package/dist/react/engine/render.js +87 -0
- package/dist/react/engine/settings.d.ts +8 -0
- package/dist/react/engine/settings.js +68 -0
- package/dist/react/engine/store.d.ts +64 -0
- package/dist/react/engine/store.js +42 -0
- package/dist/react/engine/translate.d.ts +7 -0
- package/dist/react/engine/translate.js +33 -0
- package/dist/react/engine/types.d.ts +128 -0
- package/dist/react/engine/types.js +1 -0
- package/dist/react/index.d.ts +38 -0
- package/dist/react/index.js +39 -0
- package/dist/react/registries.d.ts +10 -0
- package/dist/react/registries.js +68 -0
- package/dist/react/registries.server.d.ts +2 -0
- package/dist/react/registries.server.js +98 -0
- package/dist/react/sections/404.d.ts +10 -0
- package/dist/react/sections/404.js +10 -0
- package/dist/react/sections/announcement-bar.d.ts +14 -0
- package/dist/react/sections/announcement-bar.js +84 -0
- package/dist/react/sections/article.d.ts +17 -0
- package/dist/react/sections/article.js +31 -0
- package/dist/react/sections/contact-form.d.ts +16 -0
- package/dist/react/sections/contact-form.js +8 -0
- package/dist/react/sections/custom-liquid.d.ts +14 -0
- package/dist/react/sections/custom-liquid.js +7 -0
- package/dist/react/sections/custom-section.d.ts +14 -0
- package/dist/react/sections/custom-section.js +9 -0
- package/dist/react/sections/featured-collection.d.ts +10 -0
- package/dist/react/sections/featured-collection.js +23 -0
- package/dist/react/sections/hero.d.ts +6 -0
- package/dist/react/sections/hero.js +35 -0
- package/dist/react/sections/image-with-text.d.ts +13 -0
- package/dist/react/sections/image-with-text.js +9 -0
- package/dist/react/sections/multicolumn.d.ts +15 -0
- package/dist/react/sections/multicolumn.js +8 -0
- package/dist/react/sections/page.d.ts +13 -0
- package/dist/react/sections/page.js +11 -0
- package/dist/react/sections/password.d.ts +15 -0
- package/dist/react/sections/password.js +9 -0
- package/dist/react/sections/rich-text.d.ts +12 -0
- package/dist/react/sections/rich-text.js +7 -0
- package/dist/react/sections/stories.d.ts +18 -0
- package/dist/react/sections/stories.js +10 -0
- package/dist/react/sections/video-bubble.d.ts +13 -0
- package/dist/react/sections/video-bubble.js +54 -0
- package/dist/react/sections/video-carousel.d.ts +19 -0
- package/dist/react/sections/video-carousel.js +9 -0
- package/dist/react/server.d.ts +7 -0
- package/dist/react/server.js +42 -0
- package/dist/schemas/bridge.d.ts +98 -0
- package/dist/schemas/bridge.js +50 -0
- package/dist/schemas/data.d.ts +54 -0
- package/dist/schemas/data.js +22 -0
- package/dist/schemas/index.d.ts +14 -0
- package/dist/schemas/index.js +14 -0
- package/dist/schemas/manifest.d.ts +63 -0
- package/dist/schemas/manifest.js +32 -0
- package/dist/schemas/schema.d.ts +159 -0
- package/dist/schemas/schema.js +129 -0
- package/dist/schemas/validate.d.ts +22 -0
- package/dist/schemas/validate.js +199 -0
- package/package.json +84 -0
- package/src/commerce/countdown.ts +38 -0
- package/src/commerce/events.ts +43 -0
- package/src/commerce/facets.ts +88 -0
- package/src/commerce/index.ts +11 -0
- package/src/commerce/money.ts +120 -0
- package/src/commerce/product.ts +177 -0
- package/src/editor/frame.ts +305 -0
- package/src/editor/host.ts +101 -0
- package/src/editor/index.ts +9 -0
- package/src/react/adapter.tsx +83 -0
- package/src/react/blocks/_box.tsx +37 -0
- package/src/react/blocks/_slide.tsx +96 -0
- package/src/react/blocks/_video-card.tsx +131 -0
- package/src/react/blocks/accordion.tsx +33 -0
- package/src/react/blocks/button.tsx +36 -0
- package/src/react/blocks/countdown.tsx +105 -0
- package/src/react/blocks/custom-liquid.tsx +21 -0
- package/src/react/blocks/group.tsx +32 -0
- package/src/react/blocks/image.tsx +29 -0
- package/src/react/blocks/message.tsx +28 -0
- package/src/react/blocks/quote.tsx +24 -0
- package/src/react/blocks/spacer.tsx +17 -0
- package/src/react/blocks/story.tsx +40 -0
- package/src/react/blocks/text.tsx +24 -0
- package/src/react/blocks/video.tsx +132 -0
- package/src/react/components/Facets.tsx +424 -0
- package/src/react/components/Icon.tsx +96 -0
- package/src/react/components/PaymentIcons.generated.ts +24 -0
- package/src/react/components/PaymentIcons.tsx +27 -0
- package/src/react/components/Price.tsx +72 -0
- package/src/react/components/ProductBadges.tsx +95 -0
- package/src/react/components/ProductCard.tsx +417 -0
- package/src/react/components/Rating.tsx +59 -0
- package/src/react/components/SwatchStyle.tsx +50 -0
- package/src/react/components/VideoModal.tsx +236 -0
- package/src/react/engine/CssVariables.tsx +142 -0
- package/src/react/engine/context.tsx +108 -0
- package/src/react/engine/fonts.ts +42 -0
- package/src/react/engine/images.tsx +58 -0
- package/src/react/engine/render.tsx +190 -0
- package/src/react/engine/settings.ts +89 -0
- package/src/react/engine/store.ts +103 -0
- package/src/react/engine/translate.ts +40 -0
- package/src/react/engine/types.ts +142 -0
- package/src/react/index.ts +78 -0
- package/src/react/registries.server.ts +110 -0
- package/src/react/registries.ts +80 -0
- package/src/react/sections/404.tsx +37 -0
- package/src/react/sections/announcement-bar.tsx +170 -0
- package/src/react/sections/article.tsx +97 -0
- package/src/react/sections/contact-form.tsx +104 -0
- package/src/react/sections/custom-liquid.tsx +39 -0
- package/src/react/sections/custom-section.tsx +44 -0
- package/src/react/sections/featured-collection.tsx +89 -0
- package/src/react/sections/hero.tsx +84 -0
- package/src/react/sections/image-with-text.tsx +61 -0
- package/src/react/sections/multicolumn.tsx +46 -0
- package/src/react/sections/page.tsx +43 -0
- package/src/react/sections/password.tsx +60 -0
- package/src/react/sections/rich-text.tsx +37 -0
- package/src/react/sections/stories.tsx +51 -0
- package/src/react/sections/video-bubble.tsx +145 -0
- package/src/react/sections/video-carousel.tsx +57 -0
- package/src/react/server.ts +60 -0
- package/src/schemas/bridge.ts +143 -0
- package/src/schemas/data.ts +72 -0
- package/src/schemas/index.ts +14 -0
- package/src/schemas/manifest.ts +85 -0
- package/src/schemas/schema.ts +287 -0
- package/src/schemas/validate.ts +302 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The rendering mirror of Shopify's section/blocks runtime:
|
|
3
|
+
*
|
|
4
|
+
* - <ThemeTemplate name="index" …/> ≙ {{ content_for_layout }} for a JSON template
|
|
5
|
+
* - <SectionGroup name="header-group"/> ≙ {% sections 'header-group' %}
|
|
6
|
+
* - Child blocks arrive as `children` ≙ {% content_for 'blocks' %}
|
|
7
|
+
*
|
|
8
|
+
* Sections are wrapped in `.shopify-section` divs and blocks in
|
|
9
|
+
* `.shopify-block` divs, matching the wrappers Shopify emits — the
|
|
10
|
+
* extracted component CSS relies on them.
|
|
11
|
+
*
|
|
12
|
+
* Every wrapper also carries a `data-z-path` attribute (the wire form
|
|
13
|
+
* of a @zalify/storefront-kit/schemas ThemeJsonPath) identifying the node in
|
|
14
|
+
* the theme JSON. The canvas-editor bridge hit-tests against it; it is
|
|
15
|
+
* emitted unconditionally so server and client markup stay identical.
|
|
16
|
+
*/
|
|
17
|
+
import {formatThemePath, DATA_PATH_ATTR} from '../../schemas/index.ts';
|
|
18
|
+
import {getSectionGroup, getTemplate, getThemeStore} from './store';
|
|
19
|
+
import {SectionProvider, TemplateProvider} from './context';
|
|
20
|
+
import type {BlockData, SectionData, TemplateData} from './types';
|
|
21
|
+
|
|
22
|
+
/** The scope half of a ThemeJsonPath, threaded through rendering. */
|
|
23
|
+
export interface RenderScope {
|
|
24
|
+
kind: 'template' | 'group';
|
|
25
|
+
name: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const warned = new Set<string>();
|
|
29
|
+
function warnMissing(kind: 'section' | 'block', type: string) {
|
|
30
|
+
const key = `${kind}:${type}`;
|
|
31
|
+
if (warned.has(key)) return;
|
|
32
|
+
warned.add(key);
|
|
33
|
+
// eslint-disable-next-line no-console
|
|
34
|
+
console.warn(`[theme] No ${kind} component registered for type "${type}"`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function pathAttr(
|
|
38
|
+
scope: RenderScope | undefined,
|
|
39
|
+
sectionKey: string,
|
|
40
|
+
blockKeys: string[],
|
|
41
|
+
): Record<string, string> {
|
|
42
|
+
if (!scope) return {};
|
|
43
|
+
return {
|
|
44
|
+
[DATA_PATH_ATTR]: formatThemePath({scope, sectionKey, blockKeys}),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function RenderBlocks({
|
|
49
|
+
container,
|
|
50
|
+
sectionId,
|
|
51
|
+
scope,
|
|
52
|
+
parentBlockKeys = [],
|
|
53
|
+
}: {
|
|
54
|
+
container: {blocks?: Record<string, BlockData>; block_order?: string[]};
|
|
55
|
+
sectionId: string;
|
|
56
|
+
scope?: RenderScope;
|
|
57
|
+
parentBlockKeys?: string[];
|
|
58
|
+
}) {
|
|
59
|
+
const {blocks: blockRegistry} = getThemeStore();
|
|
60
|
+
const order = container.block_order ?? [];
|
|
61
|
+
if (!order.length || !container.blocks) return null;
|
|
62
|
+
const visible = order.filter((key) => {
|
|
63
|
+
const block = container.blocks![key];
|
|
64
|
+
return block && !block.disabled;
|
|
65
|
+
});
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
{visible.map((key, index) => {
|
|
69
|
+
const block = container.blocks![key];
|
|
70
|
+
const Component = blockRegistry[block.type];
|
|
71
|
+
if (!Component) {
|
|
72
|
+
warnMissing('block', block.type);
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const blockKeys = [...parentBlockKeys, key];
|
|
76
|
+
return (
|
|
77
|
+
<div
|
|
78
|
+
key={key}
|
|
79
|
+
className="shopify-block"
|
|
80
|
+
data-block-type={block.type}
|
|
81
|
+
{...pathAttr(scope, sectionId, blockKeys)}
|
|
82
|
+
>
|
|
83
|
+
<Component
|
|
84
|
+
id={key}
|
|
85
|
+
block={block}
|
|
86
|
+
settings={block.settings ?? {}}
|
|
87
|
+
index={index}
|
|
88
|
+
total={visible.length}
|
|
89
|
+
sectionId={sectionId}
|
|
90
|
+
>
|
|
91
|
+
<RenderBlocks
|
|
92
|
+
container={block}
|
|
93
|
+
sectionId={sectionId}
|
|
94
|
+
scope={scope}
|
|
95
|
+
parentBlockKeys={blockKeys}
|
|
96
|
+
/>
|
|
97
|
+
</Component>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
})}
|
|
101
|
+
</>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function RenderSection({
|
|
106
|
+
id,
|
|
107
|
+
section,
|
|
108
|
+
scope,
|
|
109
|
+
}: {
|
|
110
|
+
id: string;
|
|
111
|
+
section: SectionData;
|
|
112
|
+
scope?: RenderScope;
|
|
113
|
+
}) {
|
|
114
|
+
if (section.disabled) return null;
|
|
115
|
+
const Component = getThemeStore().sections[section.type];
|
|
116
|
+
if (!Component) {
|
|
117
|
+
warnMissing('section', section.type);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
id={`shopify-section-${id}`}
|
|
123
|
+
className={`shopify-section shopify-section--${section.type}`}
|
|
124
|
+
{...pathAttr(scope, id, [])}
|
|
125
|
+
>
|
|
126
|
+
<SectionProvider id={id}>
|
|
127
|
+
<Component id={id} section={section} settings={section.settings ?? {}}>
|
|
128
|
+
<RenderBlocks container={section} sectionId={id} scope={scope} />
|
|
129
|
+
</Component>
|
|
130
|
+
</SectionProvider>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function RenderSections({
|
|
136
|
+
template,
|
|
137
|
+
scope,
|
|
138
|
+
}: {
|
|
139
|
+
template: TemplateData;
|
|
140
|
+
scope?: RenderScope;
|
|
141
|
+
}) {
|
|
142
|
+
return (
|
|
143
|
+
<>
|
|
144
|
+
{template.order.map((key) => {
|
|
145
|
+
const section = template.sections[key];
|
|
146
|
+
if (!section) return null;
|
|
147
|
+
return (
|
|
148
|
+
<RenderSection key={key} id={key} section={section} scope={scope} />
|
|
149
|
+
);
|
|
150
|
+
})}
|
|
151
|
+
</>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Render a page template by name with its route resources and
|
|
157
|
+
* per-section loader data (from loadSectionData in the server module).
|
|
158
|
+
*/
|
|
159
|
+
export function ThemeTemplate({
|
|
160
|
+
name,
|
|
161
|
+
resources = {},
|
|
162
|
+
sectionData = {},
|
|
163
|
+
}: {
|
|
164
|
+
name: string;
|
|
165
|
+
resources?: Record<string, unknown>;
|
|
166
|
+
sectionData?: Record<string, unknown>;
|
|
167
|
+
}) {
|
|
168
|
+
const template = getTemplate(name);
|
|
169
|
+
return (
|
|
170
|
+
<TemplateProvider value={{name, kind: 'template', resources, sectionData}}>
|
|
171
|
+
<RenderSections template={template} scope={{kind: 'template', name}} />
|
|
172
|
+
</TemplateProvider>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Mirror of `{% sections 'header-group' %}` in layout/theme.liquid. */
|
|
177
|
+
export function SectionGroup({
|
|
178
|
+
name,
|
|
179
|
+
sectionData = {},
|
|
180
|
+
}: {
|
|
181
|
+
name: string;
|
|
182
|
+
sectionData?: Record<string, unknown>;
|
|
183
|
+
}) {
|
|
184
|
+
const group = getSectionGroup(name);
|
|
185
|
+
return (
|
|
186
|
+
<TemplateProvider value={{name, kind: 'group', resources: {}, sectionData}}>
|
|
187
|
+
<RenderSections template={group} scope={{kind: 'group', name}} />
|
|
188
|
+
</TemplateProvider>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves global theme settings the same way Shopify does: schema
|
|
3
|
+
* defaults from config/settings_schema.json, overlaid with the current
|
|
4
|
+
* preset (or flat values) in config/settings_data.json — both provided
|
|
5
|
+
* at installTheme() time.
|
|
6
|
+
*/
|
|
7
|
+
import {getThemeStore} from './store';
|
|
8
|
+
import type {ColorScheme, ThemeSettings} from './types';
|
|
9
|
+
|
|
10
|
+
interface Resolved {
|
|
11
|
+
settings: ThemeSettings;
|
|
12
|
+
schemes: ColorScheme[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let cache: Resolved | null = null;
|
|
16
|
+
|
|
17
|
+
function resolve(): Resolved {
|
|
18
|
+
if (cache) return cache;
|
|
19
|
+
const store = getThemeStore();
|
|
20
|
+
const {settingsSchema, settingsData} = store.schema;
|
|
21
|
+
|
|
22
|
+
const defaults: Record<string, unknown> = {};
|
|
23
|
+
for (const group of settingsSchema) {
|
|
24
|
+
for (const setting of group.settings ?? []) {
|
|
25
|
+
const id = setting.id as string | undefined;
|
|
26
|
+
if (!id || setting.type === 'color_scheme_group') continue;
|
|
27
|
+
if ('default' in setting) defaults[id] = setting.default;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const current =
|
|
32
|
+
typeof settingsData.current === 'string'
|
|
33
|
+
? (settingsData.presets?.[settingsData.current] ?? {})
|
|
34
|
+
: (settingsData.current ?? {});
|
|
35
|
+
|
|
36
|
+
const settings = {
|
|
37
|
+
...defaults,
|
|
38
|
+
...current,
|
|
39
|
+
...store.settingsOverride,
|
|
40
|
+
} as ThemeSettings;
|
|
41
|
+
|
|
42
|
+
const rawSchemes = settings.color_schemes as
|
|
43
|
+
| Record<string, {settings: ColorScheme['settings']}>
|
|
44
|
+
| undefined;
|
|
45
|
+
let schemes: ColorScheme[];
|
|
46
|
+
if (rawSchemes) {
|
|
47
|
+
schemes = Object.entries(rawSchemes).map(([id, value]) => ({
|
|
48
|
+
id,
|
|
49
|
+
settings: value.settings,
|
|
50
|
+
}));
|
|
51
|
+
} else {
|
|
52
|
+
// Fall back to the schema definition defaults as a single scheme.
|
|
53
|
+
const group = settingsSchema
|
|
54
|
+
.flatMap((g) => g.settings ?? [])
|
|
55
|
+
.find((s) => s.type === 'color_scheme_group');
|
|
56
|
+
const definition = (group?.definition ?? []) as Array<{
|
|
57
|
+
id: string;
|
|
58
|
+
default?: string;
|
|
59
|
+
}>;
|
|
60
|
+
const schemeSettings = Object.fromEntries(
|
|
61
|
+
definition.map((d) => [d.id, d.default ?? '#000000']),
|
|
62
|
+
) as unknown as ColorScheme['settings'];
|
|
63
|
+
schemes = [{id: 'scheme-1', settings: schemeSettings}];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cache = {settings, schemes};
|
|
67
|
+
return cache;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Global theme settings. A lazy proxy so call sites can keep reading
|
|
72
|
+
* `themeSettings.card_density` while resolution waits for installTheme().
|
|
73
|
+
*/
|
|
74
|
+
export const themeSettings: ThemeSettings = new Proxy({} as ThemeSettings, {
|
|
75
|
+
get: (_target, key) => resolve().settings[key as keyof ThemeSettings],
|
|
76
|
+
has: (_target, key) => key in resolve().settings,
|
|
77
|
+
ownKeys: () => Reflect.ownKeys(resolve().settings),
|
|
78
|
+
getOwnPropertyDescriptor: (_target, key) =>
|
|
79
|
+
Object.getOwnPropertyDescriptor(resolve().settings, key),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export function colorSchemes(): ColorScheme[] {
|
|
83
|
+
return resolve().schemes;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getColorScheme(id: string | undefined): ColorScheme {
|
|
87
|
+
const schemes = resolve().schemes;
|
|
88
|
+
return schemes.find((s) => s.id === id) ?? schemes[0];
|
|
89
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine's data store. Apps install the theme schema (the JSON
|
|
3
|
+
* copied from apps/liquid by `pnpm sync`) plus their component
|
|
4
|
+
* registries once at startup:
|
|
5
|
+
*
|
|
6
|
+
* // app/lib/theme-setup.ts — imported for side effects by root
|
|
7
|
+
* installTheme({schema, sections, blocks, sectionLoaders});
|
|
8
|
+
*
|
|
9
|
+
* No import.meta.glob in here — data loading is the app's business
|
|
10
|
+
* (Vite apps glob, other bundlers can use generated modules), which
|
|
11
|
+
* keeps this package bundler-agnostic.
|
|
12
|
+
*/
|
|
13
|
+
import type {
|
|
14
|
+
BlockComponent,
|
|
15
|
+
SectionComponent,
|
|
16
|
+
SectionGroupData,
|
|
17
|
+
SectionLoader,
|
|
18
|
+
TemplateData,
|
|
19
|
+
} from './types';
|
|
20
|
+
|
|
21
|
+
export interface ThemeSchema {
|
|
22
|
+
/** templates/*.json keyed by name ("index", "product"…). */
|
|
23
|
+
templates: Record<string, TemplateData>;
|
|
24
|
+
/** templates/customers/*.json keyed by name ("login"…). */
|
|
25
|
+
customerTemplates?: Record<string, TemplateData>;
|
|
26
|
+
/** sections/*-group.json keyed by file name ("header-group"…). */
|
|
27
|
+
sectionGroups: Record<string, SectionGroupData>;
|
|
28
|
+
/** config/settings_schema.json. */
|
|
29
|
+
settingsSchema: Array<{name: string; settings?: Array<Record<string, unknown>>}>;
|
|
30
|
+
/** config/settings_data.json. */
|
|
31
|
+
settingsData: {
|
|
32
|
+
current: string | Record<string, unknown>;
|
|
33
|
+
presets?: Record<string, Record<string, unknown>>;
|
|
34
|
+
};
|
|
35
|
+
/** locales/*.json keyed by file name ("en.default", "en.default.schema"). */
|
|
36
|
+
locales: Record<string, Record<string, unknown>>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface InstallThemeOptions {
|
|
40
|
+
schema: ThemeSchema;
|
|
41
|
+
/** Full section registry (spread builtinSections, then app extras). */
|
|
42
|
+
sections: Record<string, SectionComponent>;
|
|
43
|
+
/** Full block registry (spread builtinBlocks, then app extras). */
|
|
44
|
+
blocks: Record<string, BlockComponent>;
|
|
45
|
+
/** Server loaders keyed by section type (spread builtinSectionLoaders…). */
|
|
46
|
+
sectionLoaders?: Record<string, SectionLoader>;
|
|
47
|
+
/**
|
|
48
|
+
* App-level overrides applied on top of the resolved theme settings
|
|
49
|
+
* (schema defaults + current preset). The synced schema stays the
|
|
50
|
+
* shared source of truth; this is the seam for one app to diverge
|
|
51
|
+
* deliberately (e.g. a different Google font in one storefront).
|
|
52
|
+
*/
|
|
53
|
+
settingsOverride?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface ThemeStore extends InstallThemeOptions {}
|
|
57
|
+
|
|
58
|
+
let store: ThemeStore | null = null;
|
|
59
|
+
|
|
60
|
+
export function installTheme(options: InstallThemeOptions): void {
|
|
61
|
+
store = options;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function getThemeStore(): ThemeStore {
|
|
65
|
+
if (!store) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
'[storefront-kit] installTheme() has not been called. Import your app\'s ' +
|
|
68
|
+
'theme-setup module (which calls installTheme) before rendering.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
return store;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Look up a page template ("index", "product", "customers/login"…). */
|
|
75
|
+
export function getTemplate(name: string): TemplateData {
|
|
76
|
+
const {schema} = getThemeStore();
|
|
77
|
+
const template = name.startsWith('customers/')
|
|
78
|
+
? schema.customerTemplates?.[name.slice('customers/'.length)]
|
|
79
|
+
: schema.templates[name];
|
|
80
|
+
if (!template) throw new Error(`Unknown theme template: ${name}`);
|
|
81
|
+
return template;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Look up a section group ("header-group", "footer-group", "overlay-group"). */
|
|
85
|
+
export function getSectionGroup(name: string): SectionGroupData {
|
|
86
|
+
const group = getThemeStore().schema.sectionGroups[name];
|
|
87
|
+
if (!group) throw new Error(`Unknown section group: ${name}`);
|
|
88
|
+
return group;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Parse theme JSON that may carry Shopify's leading block comment
|
|
93
|
+
* (templates/*.json are written by the theme editor with a header).
|
|
94
|
+
* Exposed for apps that load the schema from raw strings.
|
|
95
|
+
*/
|
|
96
|
+
export function parseThemeJson<T>(raw: string, name = 'theme JSON'): T {
|
|
97
|
+
const stripped = raw.replace(/^\s*\/\*[\s\S]*?\*\//, '').trim();
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(stripped) as T;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Failed to parse ${name}: ${String(error)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirror of Liquid's `| t` filter over the theme's locale files
|
|
3
|
+
* (locales/en.default.json, provided at installTheme() time). Keys
|
|
4
|
+
* prefixed with "t:" resolve against the schema locale file
|
|
5
|
+
* (en.default.schema.json), matching the theme editor.
|
|
6
|
+
*/
|
|
7
|
+
import {getThemeStore} from './store';
|
|
8
|
+
|
|
9
|
+
type LocaleTree = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
function lookup(tree: LocaleTree, key: string): string | undefined {
|
|
12
|
+
let node: unknown = tree;
|
|
13
|
+
for (const part of key.split('.')) {
|
|
14
|
+
if (node == null || typeof node !== 'object') return undefined;
|
|
15
|
+
node = (node as LocaleTree)[part];
|
|
16
|
+
}
|
|
17
|
+
return typeof node === 'string' ? node : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Translate a locale key with `{{ var }}` interpolation.
|
|
22
|
+
*
|
|
23
|
+
* t('general.go_to_slide', {number: 2}) // "Go to slide 2"
|
|
24
|
+
* t('t:general.hero') // schema label
|
|
25
|
+
*/
|
|
26
|
+
export function t(
|
|
27
|
+
key: string,
|
|
28
|
+
vars?: Record<string, string | number>,
|
|
29
|
+
): string {
|
|
30
|
+
const {locales} = getThemeStore().schema;
|
|
31
|
+
const isSchemaKey = key.startsWith('t:');
|
|
32
|
+
const cleanKey = isSchemaKey ? key.slice(2) : key;
|
|
33
|
+
const tree = (locales[isSchemaKey ? 'en.default.schema' : 'en.default'] ??
|
|
34
|
+
{}) as LocaleTree;
|
|
35
|
+
const raw = lookup(tree, cleanKey) ?? cleanKey.split('.').pop() ?? cleanKey;
|
|
36
|
+
if (!vars) return raw;
|
|
37
|
+
return raw.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, name: string) =>
|
|
38
|
+
name in vars ? String(vars[name]) : match,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data shapes mirrored from the Zalify Liquid theme's JSON schema
|
|
3
|
+
* (templates/*.json, sections/*-group.json, config/settings_data.json).
|
|
4
|
+
* Those files are copied verbatim from the theme and are the source of
|
|
5
|
+
* truth for page structure; these types describe them.
|
|
6
|
+
*/
|
|
7
|
+
import type {ComponentType, ReactNode} from 'react';
|
|
8
|
+
|
|
9
|
+
export type SettingsBag = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
export interface BlockData {
|
|
12
|
+
type: string;
|
|
13
|
+
settings?: SettingsBag;
|
|
14
|
+
blocks?: Record<string, BlockData>;
|
|
15
|
+
block_order?: string[];
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SectionData {
|
|
20
|
+
type: string;
|
|
21
|
+
settings?: SettingsBag;
|
|
22
|
+
blocks?: Record<string, BlockData>;
|
|
23
|
+
block_order?: string[];
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
custom_css?: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TemplateData {
|
|
29
|
+
layout?: string | false;
|
|
30
|
+
wrapper?: string;
|
|
31
|
+
sections: Record<string, SectionData>;
|
|
32
|
+
order: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface SectionGroupData extends TemplateData {
|
|
36
|
+
type: string;
|
|
37
|
+
name: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** One color scheme from the color_scheme_group setting. */
|
|
41
|
+
export interface ColorScheme {
|
|
42
|
+
id: string;
|
|
43
|
+
settings: {
|
|
44
|
+
background: string;
|
|
45
|
+
text: string;
|
|
46
|
+
accent: string;
|
|
47
|
+
button: string;
|
|
48
|
+
button_label: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Global theme settings: schema defaults from config/settings_schema.json
|
|
54
|
+
* overlaid with the current preset in config/settings_data.json.
|
|
55
|
+
* Individual ids are looked up loosely; well-known ones are typed below.
|
|
56
|
+
*/
|
|
57
|
+
export interface ThemeSettings extends SettingsBag {
|
|
58
|
+
type_body_font?: string;
|
|
59
|
+
type_body_google?: string;
|
|
60
|
+
type_heading_font?: string;
|
|
61
|
+
type_heading_google?: string;
|
|
62
|
+
type_mono_google?: string;
|
|
63
|
+
max_page_width?: string;
|
|
64
|
+
min_page_margin?: number;
|
|
65
|
+
button_corner_radius?: number;
|
|
66
|
+
input_corner_radius?: number;
|
|
67
|
+
card_corner_radius?: number;
|
|
68
|
+
card_swatch_style?: 'swatch' | 'variant_image' | 'none';
|
|
69
|
+
card_density?: 'comfortable' | 'compact';
|
|
70
|
+
card_show_quick_add?: boolean;
|
|
71
|
+
card_show_secondary_image?: boolean;
|
|
72
|
+
card_show_rating?: boolean;
|
|
73
|
+
card_badge_position?: 'corner' | 'below';
|
|
74
|
+
card_badge_background?: string;
|
|
75
|
+
card_badge_text?: string;
|
|
76
|
+
card_badge_sale_background?: string;
|
|
77
|
+
card_badge_sale_text?: string;
|
|
78
|
+
badge_style_1_labels?: string;
|
|
79
|
+
badge_style_1_background?: string;
|
|
80
|
+
badge_style_1_text?: string;
|
|
81
|
+
badge_style_2_labels?: string;
|
|
82
|
+
badge_style_2_background?: string;
|
|
83
|
+
badge_style_2_text?: string;
|
|
84
|
+
favicon?: string;
|
|
85
|
+
social_instagram?: string;
|
|
86
|
+
social_tiktok?: string;
|
|
87
|
+
social_x?: string;
|
|
88
|
+
social_facebook?: string;
|
|
89
|
+
social_youtube?: string;
|
|
90
|
+
social_pinterest?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Props every ported section component (app/sections/*.tsx) receives. */
|
|
94
|
+
export interface SectionProps<S extends object = SettingsBag> {
|
|
95
|
+
/** Section key inside its template, e.g. "hero" or "main". */
|
|
96
|
+
id: string;
|
|
97
|
+
section: SectionData;
|
|
98
|
+
/** section.settings (defaults are applied by the component itself). */
|
|
99
|
+
settings: S;
|
|
100
|
+
/** Pre-rendered child blocks — the mirror of `{% content_for 'blocks' %}`. */
|
|
101
|
+
children?: ReactNode;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Props every ported block component (app/blocks/*.tsx) receives. */
|
|
105
|
+
export interface BlockProps<S extends object = SettingsBag> {
|
|
106
|
+
id: string;
|
|
107
|
+
block: BlockData;
|
|
108
|
+
settings: S;
|
|
109
|
+
/** Position among siblings — mirror of forloop/find_index tricks. */
|
|
110
|
+
index: number;
|
|
111
|
+
total: number;
|
|
112
|
+
/** Key of the section this block ultimately lives in. */
|
|
113
|
+
sectionId: string;
|
|
114
|
+
/** Pre-rendered nested blocks. */
|
|
115
|
+
children?: ReactNode;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type SectionComponent = ComponentType<SectionProps>;
|
|
119
|
+
export type BlockComponent = ComponentType<BlockProps>;
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Optional per-section server loader (app/sections/<type>.server.ts).
|
|
123
|
+
* Runs from route loaders for every instance of the section in the
|
|
124
|
+
* page's template; its result is delivered to the component via
|
|
125
|
+
* useSectionData().
|
|
126
|
+
*/
|
|
127
|
+
export interface SectionLoaderArgs {
|
|
128
|
+
settings: SettingsBag;
|
|
129
|
+
section: SectionData;
|
|
130
|
+
sectionId: string;
|
|
131
|
+
/* Storefront client from Hydrogen context (typed loosely to keep the
|
|
132
|
+
theme engine decoupled from generated types). */
|
|
133
|
+
storefront: {
|
|
134
|
+
query: (query: string, options?: Record<string, unknown>) => Promise<any>;
|
|
135
|
+
CacheLong?: () => unknown;
|
|
136
|
+
CacheShort?: () => unknown;
|
|
137
|
+
};
|
|
138
|
+
params: Record<string, string | undefined>;
|
|
139
|
+
request: Request;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export type SectionLoader = (args: SectionLoaderArgs) => Promise<unknown>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @zalify/storefront-kit/react — the Zalify theme engine and shared React
|
|
3
|
+
* components, host-framework-agnostic. An app:
|
|
4
|
+
*
|
|
5
|
+
* 1. installTheme({schema, sections, blocks, sectionLoaders}) at
|
|
6
|
+
* startup, merging builtinSections/builtinBlocks with its own
|
|
7
|
+
* app-local components;
|
|
8
|
+
* 2. wraps the tree in <AdapterProvider adapter={…}> injecting its
|
|
9
|
+
* router/cart primitives;
|
|
10
|
+
* 3. renders pages with <ThemeTemplate>/<SectionGroup> and
|
|
11
|
+
* <CssVariables/> in <head>.
|
|
12
|
+
*
|
|
13
|
+
* Server loaders: import {loadSectionData} from '@zalify/storefront-kit/react/server'.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// Engine
|
|
17
|
+
export * from './engine/types';
|
|
18
|
+
export {
|
|
19
|
+
installTheme,
|
|
20
|
+
getTemplate,
|
|
21
|
+
getSectionGroup,
|
|
22
|
+
parseThemeJson,
|
|
23
|
+
} from './engine/store';
|
|
24
|
+
export type {ThemeSchema, InstallThemeOptions} from './engine/store';
|
|
25
|
+
export {themeSettings, colorSchemes, getColorScheme} from './engine/settings';
|
|
26
|
+
export {t} from './engine/translate';
|
|
27
|
+
export {
|
|
28
|
+
ThemeTemplate,
|
|
29
|
+
SectionGroup,
|
|
30
|
+
RenderSection,
|
|
31
|
+
RenderSections,
|
|
32
|
+
} from './engine/render';
|
|
33
|
+
export {
|
|
34
|
+
ThemeDataProvider,
|
|
35
|
+
TemplateProvider,
|
|
36
|
+
useThemeGlobals,
|
|
37
|
+
useResource,
|
|
38
|
+
useSectionData,
|
|
39
|
+
useTemplateScope,
|
|
40
|
+
} from './engine/context';
|
|
41
|
+
export {CssVariables} from './engine/CssVariables';
|
|
42
|
+
export {parseFontHandle, googleFontsHref} from './engine/fonts';
|
|
43
|
+
export {
|
|
44
|
+
settingImageUrl,
|
|
45
|
+
imageUrl,
|
|
46
|
+
imageSrcSet,
|
|
47
|
+
PlaceholderSvg,
|
|
48
|
+
} from './engine/images';
|
|
49
|
+
|
|
50
|
+
// Adapter seam
|
|
51
|
+
export {AdapterProvider, useAdapter} from './adapter';
|
|
52
|
+
export type {
|
|
53
|
+
ThemeAdapter,
|
|
54
|
+
AdapterLinkProps,
|
|
55
|
+
CartAddFormProps,
|
|
56
|
+
CartLineInput,
|
|
57
|
+
} from './adapter';
|
|
58
|
+
|
|
59
|
+
// Shared components
|
|
60
|
+
export {Icon} from './components/Icon';
|
|
61
|
+
export type {IconName} from './components/Icon';
|
|
62
|
+
export {PaymentIcons} from './components/PaymentIcons';
|
|
63
|
+
export type {PaymentType} from './components/PaymentIcons';
|
|
64
|
+
export {Price} from './components/Price';
|
|
65
|
+
export {Rating} from './components/Rating';
|
|
66
|
+
export {ProductBadges, hasProductBadges} from './components/ProductBadges';
|
|
67
|
+
export {swatchStyle} from './components/SwatchStyle';
|
|
68
|
+
export {ProductCard, PRODUCT_CARD_FRAGMENT} from './components/ProductCard';
|
|
69
|
+
export * from './components/VideoModal';
|
|
70
|
+
export {
|
|
71
|
+
Facets,
|
|
72
|
+
COLLECTION_SORT_OPTIONS,
|
|
73
|
+
SEARCH_SORT_OPTIONS,
|
|
74
|
+
} from './components/Facets';
|
|
75
|
+
|
|
76
|
+
// Built-in registries (spread into installTheme with app extras).
|
|
77
|
+
// Server-only loaders live in '@zalify/storefront-kit/react/server'.
|
|
78
|
+
export {builtinSections, builtinBlocks} from './registries';
|