@shoppexio/builder-runtime 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/dist/attributes.d.ts +8 -0
- package/dist/attributes.d.ts.map +1 -0
- package/dist/attributes.js +17 -0
- package/dist/builder-runtime.test.d.ts +2 -0
- package/dist/builder-runtime.test.d.ts.map +1 -0
- package/dist/builder-runtime.test.js +115 -0
- package/dist/content.d.ts +13 -0
- package/dist/content.d.ts.map +1 -0
- package/dist/content.js +39 -0
- package/dist/css-vars.d.ts +6 -0
- package/dist/css-vars.d.ts.map +1 -0
- package/dist/css-vars.js +97 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/layout.d.ts +9 -0
- package/dist/layout.d.ts.map +1 -0
- package/dist/layout.js +40 -0
- package/dist/react-runtime.test.d.ts +2 -0
- package/dist/react-runtime.test.d.ts.map +1 -0
- package/dist/react-runtime.test.js +292 -0
- package/dist/react.d.ts +302 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +408 -0
- package/dist/style-slots.d.ts +12 -0
- package/dist/style-slots.d.ts.map +1 -0
- package/dist/style-slots.js +31 -0
- package/package.json +91 -0
- package/src/attributes.ts +23 -0
- package/src/builder-runtime.test.ts +143 -0
- package/src/content.ts +58 -0
- package/src/css-vars.ts +124 -0
- package/src/index.ts +6 -0
- package/src/jsdom.d.ts +6 -0
- package/src/layout.ts +55 -0
- package/src/react-runtime.test.tsx +430 -0
- package/src/react.tsx +550 -0
- package/src/style-slots.ts +46 -0
package/dist/react.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { BuilderSettingsSchema, PreviewMessageSchema, createEmptyBuilderSettings, migrateLegacyBuilderSettings, } from '@shoppex/builder-contracts';
|
|
3
|
+
import { createElement, createContext, useContext, useEffect, useMemo, useRef, useState, } from 'react';
|
|
4
|
+
import { getBlockSettingValue, getBuilderContentList, getBuilderContentRecord, getBuilderContentString, getBuilderContentValue, } from './content.js';
|
|
5
|
+
import { createBuilderCss } from './css-vars.js';
|
|
6
|
+
import { getPageBlocks, getPageLayout, getVisiblePageBlocks } from './layout.js';
|
|
7
|
+
import { resolveStyleSlotValue } from './style-slots.js';
|
|
8
|
+
const BUILDER_SETTINGS_WINDOW_KEY = '__SHOPPEX_BUILDER_SETTINGS__';
|
|
9
|
+
const BUILDER_STATE_EVENT_NAME = 'shoppex:builder-state';
|
|
10
|
+
const BUILDER_BLOCK_SELECTOR = '[data-builder-block]';
|
|
11
|
+
const BUILDER_CONTENT_SELECTOR = '[data-builder-content]';
|
|
12
|
+
const BUILDER_SELECTED_SELECTOR = '[data-builder-selected="true"]';
|
|
13
|
+
const BuilderRuntimeContext = createContext(null);
|
|
14
|
+
const BuilderBlockContext = createContext(null);
|
|
15
|
+
export function BuilderRuntimeProvider({ settings, children }) {
|
|
16
|
+
const value = useMemo(() => ({ settings }), [settings]);
|
|
17
|
+
return _jsx(BuilderRuntimeContext.Provider, { value: value, children: children });
|
|
18
|
+
}
|
|
19
|
+
export function BuilderBlockProvider({ block, children }) {
|
|
20
|
+
return _jsx(BuilderBlockContext.Provider, { value: block, children: children });
|
|
21
|
+
}
|
|
22
|
+
export function BuilderRuntimePreviewProvider({ initialSettings, children, }) {
|
|
23
|
+
const [settings, setSettings] = useState(() => parseInitialBuilderSettings(initialSettings));
|
|
24
|
+
const settingsRevisionRef = useRef(settings.revision);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
settingsRevisionRef.current = settings.revision;
|
|
27
|
+
}, [settings.revision]);
|
|
28
|
+
useEffect(() => {
|
|
29
|
+
const currentWindow = window;
|
|
30
|
+
const parentOrigin = getPreviewParentOrigin(document.referrer);
|
|
31
|
+
const isTrustedPreviewEmbed = isTrustedBuilderPreviewEmbed(window.location, parentOrigin);
|
|
32
|
+
const removeInspectorStyles = isTrustedPreviewEmbed ? installBuilderPreviewInspectorStyles() : () => { };
|
|
33
|
+
const removeHoverInspector = isTrustedPreviewEmbed ? installBuilderPreviewHoverInspector() : () => { };
|
|
34
|
+
const postToParent = (event, response) => {
|
|
35
|
+
const target = event?.source && 'postMessage' in event.source ? event.source : window.parent;
|
|
36
|
+
if (!target || target === window)
|
|
37
|
+
return;
|
|
38
|
+
const targetOrigin = parentOrigin ?? event?.origin ?? '*';
|
|
39
|
+
target.postMessage(response, targetOrigin || '*');
|
|
40
|
+
};
|
|
41
|
+
const applySettings = (input) => {
|
|
42
|
+
const parsed = BuilderSettingsSchema.safeParse(input);
|
|
43
|
+
if (!parsed.success)
|
|
44
|
+
return { status: 'invalid' };
|
|
45
|
+
if (parsed.data.revision < settingsRevisionRef.current)
|
|
46
|
+
return { status: 'stale' };
|
|
47
|
+
settingsRevisionRef.current = parsed.data.revision;
|
|
48
|
+
setSettings(parsed.data);
|
|
49
|
+
return { status: 'applied', settings: parsed.data };
|
|
50
|
+
};
|
|
51
|
+
applySettings(currentWindow[BUILDER_SETTINGS_WINDOW_KEY]);
|
|
52
|
+
const handleBuilderState = (event) => {
|
|
53
|
+
const detail = event instanceof CustomEvent ? event.detail : null;
|
|
54
|
+
applySettings(detail);
|
|
55
|
+
};
|
|
56
|
+
const handlePreviewMessage = (event) => {
|
|
57
|
+
if (!isTrustedPreviewEmbed)
|
|
58
|
+
return;
|
|
59
|
+
if (parentOrigin && event.origin !== parentOrigin)
|
|
60
|
+
return;
|
|
61
|
+
if (!parentOrigin && event.source !== window.parent)
|
|
62
|
+
return;
|
|
63
|
+
const parsed = PreviewMessageSchema.safeParse(event.data);
|
|
64
|
+
if (!parsed.success)
|
|
65
|
+
return;
|
|
66
|
+
const message = parsed.data;
|
|
67
|
+
if (message.type === 'REQUEST_READY') {
|
|
68
|
+
postToParent(event, { type: 'READY', revision: settingsRevisionRef.current });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (message.type === 'APPLY_STATE') {
|
|
72
|
+
const result = applySettings(message.state);
|
|
73
|
+
postToParent(event, result.status === 'applied'
|
|
74
|
+
? { type: 'APPLIED', revision: message.revision }
|
|
75
|
+
: {
|
|
76
|
+
type: 'APPLY_FAILED',
|
|
77
|
+
revision: message.revision,
|
|
78
|
+
error: result.status === 'stale' ? 'Stale builder revision' : 'Invalid builder state message',
|
|
79
|
+
});
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (message.type === 'RELOAD') {
|
|
83
|
+
postToParent(event, { type: 'APPLIED', revision: message.revision });
|
|
84
|
+
window.setTimeout(() => window.location.reload(), 0);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
if (message.type === 'SELECT_ELEMENT') {
|
|
88
|
+
selectBuilderElement(message.selection.blockId);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const handleBuilderClick = (event) => {
|
|
92
|
+
if (!isTrustedPreviewEmbed)
|
|
93
|
+
return;
|
|
94
|
+
const target = event.target;
|
|
95
|
+
if (!(target instanceof Element))
|
|
96
|
+
return;
|
|
97
|
+
const blockElement = target.closest(BUILDER_BLOCK_SELECTOR);
|
|
98
|
+
if (!blockElement)
|
|
99
|
+
return;
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
const selection = createBuilderSelection(blockElement, target);
|
|
103
|
+
if (!selection.blockId)
|
|
104
|
+
return;
|
|
105
|
+
postToParent(null, {
|
|
106
|
+
type: 'ELEMENT_CLICKED',
|
|
107
|
+
revision: settingsRevisionRef.current,
|
|
108
|
+
selection,
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
if (isTrustedPreviewEmbed) {
|
|
112
|
+
postToParent(null, { type: 'READY', revision: settingsRevisionRef.current });
|
|
113
|
+
}
|
|
114
|
+
window.addEventListener(BUILDER_STATE_EVENT_NAME, handleBuilderState);
|
|
115
|
+
window.addEventListener('message', handlePreviewMessage);
|
|
116
|
+
window.addEventListener('click', handleBuilderClick, true);
|
|
117
|
+
return () => {
|
|
118
|
+
removeInspectorStyles();
|
|
119
|
+
removeHoverInspector();
|
|
120
|
+
window.removeEventListener(BUILDER_STATE_EVENT_NAME, handleBuilderState);
|
|
121
|
+
window.removeEventListener('message', handlePreviewMessage);
|
|
122
|
+
window.removeEventListener('click', handleBuilderClick, true);
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
return _jsx(BuilderRuntimeProvider, { settings: settings, children: children });
|
|
126
|
+
}
|
|
127
|
+
export function BuilderRuntimeStyle({ selector = ':root' }) {
|
|
128
|
+
const css = useBuilderCss(selector);
|
|
129
|
+
if (!css) {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
return _jsx("style", { "data-shoppex-builder-runtime": "v2", children: css });
|
|
133
|
+
}
|
|
134
|
+
export function BuilderBlockFrame({ as, pageId, block, className, children, }) {
|
|
135
|
+
return createElement(as ?? 'div', {
|
|
136
|
+
className,
|
|
137
|
+
'data-page-id': pageId,
|
|
138
|
+
'data-builder-block': block.id,
|
|
139
|
+
'data-builder-block-type': block.type,
|
|
140
|
+
}, children);
|
|
141
|
+
}
|
|
142
|
+
export function BuilderPage({ pageId, blocks, registry, context, fallback = null, }) {
|
|
143
|
+
const runtimeBlocks = useVisibleBuilderPageBlocks(pageId);
|
|
144
|
+
const pageBlocks = blocks ?? runtimeBlocks;
|
|
145
|
+
return (_jsx(_Fragment, { children: pageBlocks.map((block) => {
|
|
146
|
+
const Component = registry[block.type];
|
|
147
|
+
if (!Component) {
|
|
148
|
+
return fallback;
|
|
149
|
+
}
|
|
150
|
+
return (_jsx(BuilderBlockProvider, { block: block, children: _jsx(Component, { block: block, context: context }) }, block.id));
|
|
151
|
+
}) }));
|
|
152
|
+
}
|
|
153
|
+
export function useBuilderRuntime() {
|
|
154
|
+
const context = useContext(BuilderRuntimeContext);
|
|
155
|
+
if (!context) {
|
|
156
|
+
throw new Error('useBuilderRuntime must be used inside BuilderRuntimeProvider');
|
|
157
|
+
}
|
|
158
|
+
return context;
|
|
159
|
+
}
|
|
160
|
+
export function useBuilderContent(path, fallback) {
|
|
161
|
+
const scopedValue = useScopedBuilderContentValue(path);
|
|
162
|
+
if (typeof scopedValue === 'string')
|
|
163
|
+
return scopedValue;
|
|
164
|
+
return getBuilderContentString(useBuilderRuntime().settings, path, fallback);
|
|
165
|
+
}
|
|
166
|
+
export function useBuilderContentValue(path) {
|
|
167
|
+
const scopedValue = useScopedBuilderContentValue(path);
|
|
168
|
+
if (scopedValue !== undefined)
|
|
169
|
+
return scopedValue;
|
|
170
|
+
return getBuilderContentValue(useBuilderRuntime().settings, path);
|
|
171
|
+
}
|
|
172
|
+
export function useBuilderContentList(path, fallback = []) {
|
|
173
|
+
const scopedValue = useScopedBuilderContentValue(path);
|
|
174
|
+
if (Array.isArray(scopedValue))
|
|
175
|
+
return scopedValue;
|
|
176
|
+
return getBuilderContentList(useBuilderRuntime().settings, path, fallback);
|
|
177
|
+
}
|
|
178
|
+
export function useBuilderContentRecord() {
|
|
179
|
+
const block = useContext(BuilderBlockContext);
|
|
180
|
+
const content = getBuilderContentRecord(useBuilderRuntime().settings);
|
|
181
|
+
if (!block)
|
|
182
|
+
return content;
|
|
183
|
+
return {
|
|
184
|
+
...content,
|
|
185
|
+
[block.type]: {
|
|
186
|
+
...(typeof content[block.type] === 'object' && content[block.type] !== null
|
|
187
|
+
? content[block.type]
|
|
188
|
+
: {}),
|
|
189
|
+
...block.settings,
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
export function useBuilderBlockSetting(input) {
|
|
194
|
+
return getBlockSettingValue(useBuilderRuntime().settings, input);
|
|
195
|
+
}
|
|
196
|
+
export function useBuilderPageLayout(pageId) {
|
|
197
|
+
return getPageLayout(useBuilderRuntime().settings, pageId);
|
|
198
|
+
}
|
|
199
|
+
export function useBuilderPageBlocks(pageId) {
|
|
200
|
+
return getPageBlocks(useBuilderRuntime().settings, pageId);
|
|
201
|
+
}
|
|
202
|
+
export function useVisibleBuilderPageBlocks(pageId) {
|
|
203
|
+
return getVisiblePageBlocks(useBuilderRuntime().settings, pageId);
|
|
204
|
+
}
|
|
205
|
+
export function useBuilderStyleSlot(slotId, input = {}) {
|
|
206
|
+
return resolveStyleSlotValue(useBuilderRuntime().settings, slotId, input);
|
|
207
|
+
}
|
|
208
|
+
export function useBuilderCss(selector = ':root') {
|
|
209
|
+
return createBuilderCss(useBuilderRuntime().settings, selector);
|
|
210
|
+
}
|
|
211
|
+
function useScopedBuilderContentValue(path) {
|
|
212
|
+
const block = useContext(BuilderBlockContext);
|
|
213
|
+
if (!block)
|
|
214
|
+
return undefined;
|
|
215
|
+
if (Object.prototype.hasOwnProperty.call(block.settings, path)) {
|
|
216
|
+
return block.settings[path];
|
|
217
|
+
}
|
|
218
|
+
const shortPath = path.startsWith(`${block.type}.`) ? path.slice(block.type.length + 1) : path.split('.').at(-1);
|
|
219
|
+
if (shortPath && Object.prototype.hasOwnProperty.call(block.settings, shortPath)) {
|
|
220
|
+
return block.settings[shortPath];
|
|
221
|
+
}
|
|
222
|
+
const nested = shortPath ? getNestedBuilderSetting(block.settings, shortPath) : undefined;
|
|
223
|
+
return nested ?? getNestedBuilderSetting(block.settings, path);
|
|
224
|
+
}
|
|
225
|
+
function getNestedBuilderSetting(record, path) {
|
|
226
|
+
let current = record;
|
|
227
|
+
for (const segment of path.split('.')) {
|
|
228
|
+
if (!current || typeof current !== 'object') {
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
current = current[segment];
|
|
232
|
+
}
|
|
233
|
+
return current;
|
|
234
|
+
}
|
|
235
|
+
function parseInitialBuilderSettings(input) {
|
|
236
|
+
const parsed = BuilderSettingsSchema.safeParse(input);
|
|
237
|
+
if (parsed.success)
|
|
238
|
+
return parsed.data;
|
|
239
|
+
const candidate = unwrapBuilderSettingsInput(input);
|
|
240
|
+
const candidateParsed = BuilderSettingsSchema.safeParse(candidate);
|
|
241
|
+
if (candidateParsed.success)
|
|
242
|
+
return candidateParsed.data;
|
|
243
|
+
if (!isRecord(candidate) || !isRecord(candidate.theme)) {
|
|
244
|
+
return createEmptyBuilderSettings(0);
|
|
245
|
+
}
|
|
246
|
+
const revision = parseBuilderRevision(candidate.revision);
|
|
247
|
+
const migrated = migrateLegacyBuilderSettings({ theme: candidate.theme }, revision);
|
|
248
|
+
const mixedCandidate = BuilderSettingsSchema.safeParse({
|
|
249
|
+
...migrated,
|
|
250
|
+
theme: {
|
|
251
|
+
...migrated.theme,
|
|
252
|
+
pages: Array.isArray(candidate.theme.pages) ? candidate.theme.pages : migrated.theme.pages,
|
|
253
|
+
terms: isRecord(candidate.theme.terms) ? candidate.theme.terms : migrated.theme.terms,
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
return mixedCandidate.success ? mixedCandidate.data : migrated;
|
|
257
|
+
}
|
|
258
|
+
function unwrapBuilderSettingsInput(input) {
|
|
259
|
+
if (!isRecord(input))
|
|
260
|
+
return input;
|
|
261
|
+
if (isRecord(input.builder_settings))
|
|
262
|
+
return input.builder_settings;
|
|
263
|
+
if (isRecord(input.builderSettings))
|
|
264
|
+
return input.builderSettings;
|
|
265
|
+
return input;
|
|
266
|
+
}
|
|
267
|
+
function parseBuilderRevision(input) {
|
|
268
|
+
return typeof input === 'number' && Number.isInteger(input) && input >= 0 ? input : 0;
|
|
269
|
+
}
|
|
270
|
+
function isRecord(value) {
|
|
271
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
272
|
+
}
|
|
273
|
+
function getPreviewParentOrigin(referrer) {
|
|
274
|
+
if (!referrer)
|
|
275
|
+
return null;
|
|
276
|
+
try {
|
|
277
|
+
return new URL(referrer).origin;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function isTrustedBuilderPreviewEmbed(location, parentOrigin) {
|
|
284
|
+
if (window.parent === window || !parentOrigin)
|
|
285
|
+
return false;
|
|
286
|
+
if (!hasBuilderPreviewMode(location))
|
|
287
|
+
return false;
|
|
288
|
+
return isTrustedBuilderPreviewParentOrigin(parentOrigin);
|
|
289
|
+
}
|
|
290
|
+
function hasBuilderPreviewMode(location) {
|
|
291
|
+
const searchParams = new URLSearchParams(location.search);
|
|
292
|
+
return (searchParams.get('shoppex-preview-mode') === 'theme'
|
|
293
|
+
|| searchParams.get('shoppex-preview-mode') === 'builder'
|
|
294
|
+
|| searchParams.get('shoppex-preview') === 'builder');
|
|
295
|
+
}
|
|
296
|
+
function isTrustedBuilderPreviewParentOrigin(origin) {
|
|
297
|
+
try {
|
|
298
|
+
const parsed = new URL(origin);
|
|
299
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
300
|
+
return (hostname === 'dashboard.shoppex.io'
|
|
301
|
+
|| hostname === 'dashboard.shoppex.test'
|
|
302
|
+
|| hostname === 'localhost'
|
|
303
|
+
|| hostname === '127.0.0.1'
|
|
304
|
+
|| hostname === '::1'
|
|
305
|
+
|| hostname.endsWith('.localhost')
|
|
306
|
+
|| hostname.endsWith('.vercel.app')
|
|
307
|
+
|| hostname.endsWith('.vercel.run'));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
function selectBuilderElement(blockId) {
|
|
314
|
+
document
|
|
315
|
+
.querySelectorAll(BUILDER_SELECTED_SELECTOR)
|
|
316
|
+
.forEach((node) => node.removeAttribute('data-builder-selected'));
|
|
317
|
+
if (!blockId)
|
|
318
|
+
return;
|
|
319
|
+
const target = document.querySelector(`[data-builder-block="${CSS.escape(blockId)}"]`);
|
|
320
|
+
if (!target)
|
|
321
|
+
return;
|
|
322
|
+
target.setAttribute('data-builder-selected', 'true');
|
|
323
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
324
|
+
}
|
|
325
|
+
function createBuilderSelection(blockElement, target) {
|
|
326
|
+
const contentElement = target.closest(BUILDER_CONTENT_SELECTOR);
|
|
327
|
+
const slotElement = target.closest('[data-builder-slot]');
|
|
328
|
+
const linkElement = target.closest('a[href]');
|
|
329
|
+
const selection = {
|
|
330
|
+
blockId: blockElement.getAttribute('data-builder-block') ?? undefined,
|
|
331
|
+
blockType: blockElement.getAttribute('data-builder-block-type') ?? undefined,
|
|
332
|
+
pageId: blockElement.getAttribute('data-page-id') ?? undefined,
|
|
333
|
+
contentPath: contentElement?.getAttribute('data-builder-content') ?? undefined,
|
|
334
|
+
slotId: slotElement?.getAttribute('data-builder-slot') ?? undefined,
|
|
335
|
+
elementType: inferBuilderElementType(target, blockElement, contentElement, slotElement),
|
|
336
|
+
};
|
|
337
|
+
if (linkElement && blockElement.contains(linkElement)) {
|
|
338
|
+
selection.href = linkElement.getAttribute('href') ?? '';
|
|
339
|
+
selection.target = linkElement.getAttribute('target') ?? '';
|
|
340
|
+
selection.rel = linkElement.getAttribute('rel') ?? '';
|
|
341
|
+
}
|
|
342
|
+
return selection;
|
|
343
|
+
}
|
|
344
|
+
function inferBuilderElementType(target, blockElement, contentElement, slotElement) {
|
|
345
|
+
const element = contentElement ?? target;
|
|
346
|
+
if (element.closest('a[href]'))
|
|
347
|
+
return 'link';
|
|
348
|
+
if (element.closest('button'))
|
|
349
|
+
return 'button';
|
|
350
|
+
if (element instanceof HTMLImageElement || element.closest('img'))
|
|
351
|
+
return 'image';
|
|
352
|
+
if (slotElement)
|
|
353
|
+
return 'style';
|
|
354
|
+
return element === blockElement ? 'block' : 'text';
|
|
355
|
+
}
|
|
356
|
+
function installBuilderPreviewInspectorStyles() {
|
|
357
|
+
const style = document.createElement('style');
|
|
358
|
+
style.setAttribute('data-shoppex-builder-inspector', 'v2');
|
|
359
|
+
style.textContent = `
|
|
360
|
+
[data-builder-block] {
|
|
361
|
+
position: relative;
|
|
362
|
+
}
|
|
363
|
+
[data-builder-block][data-builder-selected="true"] {
|
|
364
|
+
outline: 2px solid #2563eb;
|
|
365
|
+
outline-offset: 4px;
|
|
366
|
+
}
|
|
367
|
+
[data-builder-block][data-builder-hovered="true"] {
|
|
368
|
+
outline: 1px dashed #2563eb;
|
|
369
|
+
outline-offset: 4px;
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
}
|
|
372
|
+
`;
|
|
373
|
+
document.head.appendChild(style);
|
|
374
|
+
return () => style.remove();
|
|
375
|
+
}
|
|
376
|
+
function installBuilderPreviewHoverInspector() {
|
|
377
|
+
let hovered = null;
|
|
378
|
+
const clearHovered = () => {
|
|
379
|
+
hovered?.removeAttribute('data-builder-hovered');
|
|
380
|
+
hovered = null;
|
|
381
|
+
};
|
|
382
|
+
const handleMouseOver = (event) => {
|
|
383
|
+
const target = event.target;
|
|
384
|
+
if (!(target instanceof Element))
|
|
385
|
+
return;
|
|
386
|
+
const blockElement = target.closest(BUILDER_BLOCK_SELECTOR);
|
|
387
|
+
if (blockElement === hovered)
|
|
388
|
+
return;
|
|
389
|
+
clearHovered();
|
|
390
|
+
if (!blockElement)
|
|
391
|
+
return;
|
|
392
|
+
hovered = blockElement;
|
|
393
|
+
hovered.setAttribute('data-builder-hovered', 'true');
|
|
394
|
+
};
|
|
395
|
+
const handleMouseOut = (event) => {
|
|
396
|
+
const relatedTarget = event.relatedTarget;
|
|
397
|
+
if (relatedTarget instanceof Node && hovered?.contains(relatedTarget))
|
|
398
|
+
return;
|
|
399
|
+
clearHovered();
|
|
400
|
+
};
|
|
401
|
+
window.addEventListener('mouseover', handleMouseOver, true);
|
|
402
|
+
window.addEventListener('mouseout', handleMouseOut, true);
|
|
403
|
+
return () => {
|
|
404
|
+
clearHovered();
|
|
405
|
+
window.removeEventListener('mouseover', handleMouseOver, true);
|
|
406
|
+
window.removeEventListener('mouseout', handleMouseOut, true);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { BlockInstance, BuilderSettings, Breakpoint, StyleSlotId, StyleSlots } from '@shoppex/builder-contracts';
|
|
2
|
+
export declare function getStyleSlotValue(settings: BuilderSettings, slotId: StyleSlotId, input?: {
|
|
3
|
+
block?: BlockInstance;
|
|
4
|
+
}): unknown;
|
|
5
|
+
export declare function resolveStyleSlotValue(settings: BuilderSettings, slotId: StyleSlotId, input?: {
|
|
6
|
+
block?: BlockInstance;
|
|
7
|
+
breakpoint?: Breakpoint;
|
|
8
|
+
fallback?: unknown;
|
|
9
|
+
}): unknown;
|
|
10
|
+
export declare function mergeStyleSlots(base: StyleSlots, overrides: StyleSlots | undefined): StyleSlots;
|
|
11
|
+
export declare function isResponsiveRecord(value: unknown): value is Partial<Record<Breakpoint, unknown>>;
|
|
12
|
+
//# sourceMappingURL=style-slots.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"style-slots.d.ts","sourceRoot":"","sources":["../src/style-slots.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,4BAA4B,CAAC;AAItH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,eAAe,EAAE,MAAM,EAAE,WAAW,EAAE,KAAK,GAAE;IAAE,KAAK,CAAC,EAAE,aAAa,CAAA;CAAO,GAAG,OAAO,CAEhI;AAED,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,WAAW,EACnB,KAAK,GAAE;IAAE,KAAK,CAAC,EAAE,aAAa,CAAC;IAAC,UAAU,CAAC,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAAO,GACjF,OAAO,CAkBT;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,GAAG,SAAS,GAAG,UAAU,CAK/F;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC,CAMhG"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const BREAKPOINT_ORDER = ['base', 'sm', 'md', 'lg', 'xl'];
|
|
2
|
+
export function getStyleSlotValue(settings, slotId, input = {}) {
|
|
3
|
+
return input.block?.style_overrides?.[slotId] ?? settings.theme.style_slots[slotId];
|
|
4
|
+
}
|
|
5
|
+
export function resolveStyleSlotValue(settings, slotId, input = {}) {
|
|
6
|
+
const value = getStyleSlotValue(settings, slotId, input);
|
|
7
|
+
if (!isResponsiveRecord(value)) {
|
|
8
|
+
return value ?? input.fallback;
|
|
9
|
+
}
|
|
10
|
+
const breakpoint = input.breakpoint ?? 'base';
|
|
11
|
+
const breakpointIndex = BREAKPOINT_ORDER.indexOf(breakpoint);
|
|
12
|
+
for (let index = breakpointIndex; index >= 0; index -= 1) {
|
|
13
|
+
const candidate = value[BREAKPOINT_ORDER[index]];
|
|
14
|
+
if (candidate !== undefined) {
|
|
15
|
+
return candidate;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return input.fallback;
|
|
19
|
+
}
|
|
20
|
+
export function mergeStyleSlots(base, overrides) {
|
|
21
|
+
return {
|
|
22
|
+
...base,
|
|
23
|
+
...(overrides ?? {}),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function isResponsiveRecord(value) {
|
|
27
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
return BREAKPOINT_ORDER.some((breakpoint) => Object.prototype.hasOwnProperty.call(value, breakpoint));
|
|
31
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shoppexio/builder-runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Theme-side Builder v2 runtime helpers for Shoppex storefront themes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/ShoppexIO/shoppex.git",
|
|
9
|
+
"directory": "packages/builder-runtime"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://docs.shoppex.io",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/ShoppexIO/shoppex/issues"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"registry": "https://registry.npmjs.org",
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"main": "./src/index.ts",
|
|
20
|
+
"types": "./src/index.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"bun": "./src/index.ts",
|
|
24
|
+
"types": "./src/index.ts",
|
|
25
|
+
"import": "./src/index.ts",
|
|
26
|
+
"default": "./src/index.ts"
|
|
27
|
+
},
|
|
28
|
+
"./attributes": {
|
|
29
|
+
"bun": "./src/attributes.ts",
|
|
30
|
+
"types": "./src/attributes.ts",
|
|
31
|
+
"import": "./src/attributes.ts",
|
|
32
|
+
"default": "./src/attributes.ts"
|
|
33
|
+
},
|
|
34
|
+
"./content": {
|
|
35
|
+
"bun": "./src/content.ts",
|
|
36
|
+
"types": "./src/content.ts",
|
|
37
|
+
"import": "./src/content.ts",
|
|
38
|
+
"default": "./src/content.ts"
|
|
39
|
+
},
|
|
40
|
+
"./css-vars": {
|
|
41
|
+
"bun": "./src/css-vars.ts",
|
|
42
|
+
"types": "./src/css-vars.ts",
|
|
43
|
+
"import": "./src/css-vars.ts",
|
|
44
|
+
"default": "./src/css-vars.ts"
|
|
45
|
+
},
|
|
46
|
+
"./layout": {
|
|
47
|
+
"bun": "./src/layout.ts",
|
|
48
|
+
"types": "./src/layout.ts",
|
|
49
|
+
"import": "./src/layout.ts",
|
|
50
|
+
"default": "./src/layout.ts"
|
|
51
|
+
},
|
|
52
|
+
"./react": {
|
|
53
|
+
"bun": "./src/react.tsx",
|
|
54
|
+
"types": "./src/react.tsx",
|
|
55
|
+
"import": "./src/react.tsx",
|
|
56
|
+
"default": "./src/react.tsx"
|
|
57
|
+
},
|
|
58
|
+
"./style-slots": {
|
|
59
|
+
"bun": "./src/style-slots.ts",
|
|
60
|
+
"types": "./src/style-slots.ts",
|
|
61
|
+
"import": "./src/style-slots.ts",
|
|
62
|
+
"default": "./src/style-slots.ts"
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
"files": [
|
|
66
|
+
"dist",
|
|
67
|
+
"src"
|
|
68
|
+
],
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsc",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"test": "bun test ./src",
|
|
73
|
+
"clean": "rm -rf dist"
|
|
74
|
+
},
|
|
75
|
+
"author": "Shoppex",
|
|
76
|
+
"license": "MIT",
|
|
77
|
+
"peerDependencies": {
|
|
78
|
+
"@shoppexio/builder-contracts": "0.1.0",
|
|
79
|
+
"react": "^18.0.0 || ^19.0.0"
|
|
80
|
+
},
|
|
81
|
+
"dependencies": {
|
|
82
|
+
"@shoppex/builder-contracts": "npm:@shoppexio/builder-contracts@0.1.0"
|
|
83
|
+
},
|
|
84
|
+
"devDependencies": {
|
|
85
|
+
"jsdom": "^28.1.0",
|
|
86
|
+
"react-dom": "^19.2.4",
|
|
87
|
+
"@types/react": "^19.2.14",
|
|
88
|
+
"react": "^19.2.4",
|
|
89
|
+
"typescript": "^5.8.3"
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { StyleSlotId } from '@shoppex/builder-contracts';
|
|
2
|
+
|
|
3
|
+
export type BuilderAttributeMap = Record<string, string>;
|
|
4
|
+
|
|
5
|
+
export function builderContent(path: string): BuilderAttributeMap {
|
|
6
|
+
return {
|
|
7
|
+
'data-builder-content': path,
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function builderSlot(slotId: StyleSlotId, input: { blockId?: string } = {}): BuilderAttributeMap {
|
|
12
|
+
return {
|
|
13
|
+
'data-builder-slot': slotId,
|
|
14
|
+
...(input.blockId ? { 'data-builder-block': input.blockId } : {}),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function builderBlock(blockId: string, blockType: string): BuilderAttributeMap {
|
|
19
|
+
return {
|
|
20
|
+
'data-builder-block': blockId,
|
|
21
|
+
'data-builder-block-type': blockType,
|
|
22
|
+
};
|
|
23
|
+
}
|