@ecomiq/storefront 2.0.18 → 2.0.19

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ecomiq/storefront",
3
- "version": "2.0.18",
3
+ "version": "2.0.19",
4
4
  "description": "Micro framework SSR de Ecomiq para storefronts. Páginas, datos y deploy sin configurar nada.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -50,6 +50,7 @@
50
50
  "bin",
51
51
  "src",
52
52
  "templates",
53
+ "public",
53
54
  "README.md"
54
55
  ],
55
56
  "sideEffects": [
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Puente de modo design (solo dev). El parent (frontend) envía postMessage;
3
+ * este script pinta hover/select y devuelve metadata del nodo.
4
+ */
5
+ (function () {
6
+ if (typeof window === 'undefined' || window.parent === window) return;
7
+
8
+ var enabled = false;
9
+ var selectedEl = null;
10
+ var selectionLocked = false;
11
+ var previewStyles = {};
12
+ var hoverBox = null;
13
+ var hoverLabel = null;
14
+ var selectBox = null;
15
+ var selectLabel = null;
16
+
17
+ function ensureOverlay() {
18
+ if (hoverBox) return;
19
+ var style = document.createElement('style');
20
+ style.textContent =
21
+ '#__design_hover__,#__design_select__{pointer-events:none;position:fixed;z-index:2147483646;box-sizing:border-box;transition:top .05s,left .05s,width .05s,height .05s}' +
22
+ '#__design_hover__{border:2px solid #6366f1;background:rgba(99,102,241,.1)}' +
23
+ '#__design_select__{border:2px solid #c8a96e;background:rgba(200,169,110,.12)}' +
24
+ '.__design_lbl{position:fixed;z-index:2147483647;font:11px/1.3 ui-monospace,monospace;padding:2px 6px;border-radius:4px;white-space:nowrap;pointer-events:none}' +
25
+ '.__design_lbl_hover{background:#6366f1;color:#fff}' +
26
+ '.__design_lbl_select{background:#c8a96e;color:#1a1714}';
27
+ document.head.appendChild(style);
28
+
29
+ hoverBox = document.createElement('div');
30
+ hoverBox.id = '__design_hover__';
31
+ hoverBox.hidden = true;
32
+ hoverLabel = document.createElement('div');
33
+ hoverLabel.className = '__design_lbl __design_lbl_hover';
34
+ hoverLabel.hidden = true;
35
+
36
+ selectBox = document.createElement('div');
37
+ selectBox.id = '__design_select__';
38
+ selectBox.hidden = true;
39
+ selectLabel = document.createElement('div');
40
+ selectLabel.className = '__design_lbl __design_lbl_select';
41
+ selectLabel.hidden = true;
42
+
43
+ document.body.appendChild(hoverBox);
44
+ document.body.appendChild(hoverLabel);
45
+ document.body.appendChild(selectBox);
46
+ document.body.appendChild(selectLabel);
47
+ }
48
+
49
+ function escapeCss(value) {
50
+ if (typeof CSS !== 'undefined' && CSS.escape) return CSS.escape(value);
51
+ return value.replace(/[^a-zA-Z0-9_-]/g, '\\$&');
52
+ }
53
+
54
+ function pickElement(raw) {
55
+ if (!raw || raw === document.body || raw === document.documentElement) return null;
56
+ if (raw.id === '__design_hover__' || raw.id === '__design_select__') return null;
57
+ if (raw.classList && raw.classList.contains('__design_lbl')) return null;
58
+ return raw;
59
+ }
60
+
61
+ function getLabel(el) {
62
+ var tag = el.tagName.toLowerCase();
63
+ var classes = Array.from(el.classList || []).filter(function (c) {
64
+ return !c.startsWith('__design');
65
+ });
66
+ if (classes.length) return tag + '.' + classes.slice(0, 2).join('.');
67
+ return tag;
68
+ }
69
+
70
+ function getSelector(el) {
71
+ if (el.id) return '#' + escapeCss(el.id);
72
+ var parts = [];
73
+ var current = el;
74
+ while (current && current !== document.body && current !== document.documentElement) {
75
+ var part = current.tagName.toLowerCase();
76
+ if (current.id) {
77
+ parts.unshift('#' + escapeCss(current.id));
78
+ break;
79
+ }
80
+ var parent = current.parentElement;
81
+ if (parent) {
82
+ var siblings = Array.from(parent.children).filter(function (c) {
83
+ return c.tagName === current.tagName;
84
+ });
85
+ if (siblings.length > 1) {
86
+ var idx = siblings.indexOf(current) + 1;
87
+ part += ':nth-of-type(' + idx + ')';
88
+ }
89
+ }
90
+ var cls = Array.from(current.classList || []).find(function (c) {
91
+ return !c.startsWith('__design') && !c.startsWith('design-');
92
+ });
93
+ if (cls) part += '.' + escapeCss(cls);
94
+ parts.unshift(part);
95
+ current = parent;
96
+ }
97
+ return parts.join(' > ');
98
+ }
99
+
100
+ function textSnippet(el) {
101
+ var text = (el.innerText || el.textContent || '').replace(/\s+/g, ' ').trim();
102
+ if (!text) return undefined;
103
+ return text.length > 80 ? text.slice(0, 80) + '…' : text;
104
+ }
105
+
106
+ function camelToKebab(key) {
107
+ return key.replace(/[A-Z]/g, function (m) {
108
+ return '-' + m.toLowerCase();
109
+ });
110
+ }
111
+
112
+ function computedSnapshot(el) {
113
+ var cs = window.getComputedStyle(el);
114
+ return {
115
+ display: cs.display,
116
+ paddingTop: cs.paddingTop,
117
+ paddingRight: cs.paddingRight,
118
+ paddingBottom: cs.paddingBottom,
119
+ paddingLeft: cs.paddingLeft,
120
+ marginTop: cs.marginTop,
121
+ marginRight: cs.marginRight,
122
+ marginBottom: cs.marginBottom,
123
+ marginLeft: cs.marginLeft,
124
+ fontSize: cs.fontSize,
125
+ fontWeight: cs.fontWeight,
126
+ lineHeight: cs.lineHeight,
127
+ letterSpacing: cs.letterSpacing,
128
+ color: cs.color,
129
+ backgroundColor: cs.backgroundColor,
130
+ borderWidth: cs.borderWidth,
131
+ borderStyle: cs.borderStyle,
132
+ borderColor: cs.borderColor,
133
+ borderRadius: cs.borderRadius,
134
+ opacity: cs.opacity,
135
+ boxShadow: cs.boxShadow,
136
+ textAlign: cs.textAlign,
137
+ };
138
+ }
139
+
140
+ function elementRect(el) {
141
+ var r = el.getBoundingClientRect();
142
+ return {
143
+ top: r.top,
144
+ left: r.left,
145
+ width: r.width,
146
+ height: r.height,
147
+ };
148
+ }
149
+
150
+ function elementPayload(el) {
151
+ return {
152
+ selector: getSelector(el),
153
+ tag: el.tagName.toLowerCase(),
154
+ label: getLabel(el),
155
+ classes: Array.from(el.classList || []).filter(function (c) {
156
+ return !c.startsWith('__design');
157
+ }),
158
+ textSnippet: textSnippet(el),
159
+ computedStyles: computedSnapshot(el),
160
+ rect: elementRect(el),
161
+ };
162
+ }
163
+
164
+ var rectRaf = null;
165
+ function postSelectionRect() {
166
+ if (!selectionLocked || !selectedEl) return;
167
+ if (rectRaf) return;
168
+ rectRaf = requestAnimationFrame(function () {
169
+ rectRaf = null;
170
+ post('DESIGN_RECT', { rect: elementRect(selectedEl) });
171
+ });
172
+ }
173
+
174
+ function clearPreviewStyles() {
175
+ if (!selectedEl) return;
176
+ Object.keys(previewStyles).forEach(function (key) {
177
+ selectedEl.style.removeProperty(camelToKebab(key));
178
+ });
179
+ previewStyles = {};
180
+ updateSelectOverlay();
181
+ }
182
+
183
+ function applyPreviewStyles(styles) {
184
+ if (!selectedEl || !styles) return;
185
+ Object.keys(styles).forEach(function (key) {
186
+ var val = styles[key];
187
+ if (val === '' || val == null) {
188
+ selectedEl.style.removeProperty(camelToKebab(key));
189
+ delete previewStyles[key];
190
+ } else {
191
+ selectedEl.style.setProperty(camelToKebab(key), String(val));
192
+ previewStyles[key] = String(val);
193
+ }
194
+ });
195
+ updateSelectOverlay();
196
+ }
197
+
198
+ function placeBox(box, label, el) {
199
+ var rect = el.getBoundingClientRect();
200
+ box.style.top = rect.top + 'px';
201
+ box.style.left = rect.left + 'px';
202
+ box.style.width = rect.width + 'px';
203
+ box.style.height = rect.height + 'px';
204
+ box.hidden = false;
205
+
206
+ label.textContent = getLabel(el);
207
+ label.style.top = Math.max(4, rect.top - 22) + 'px';
208
+ label.style.left = rect.left + 'px';
209
+ label.hidden = false;
210
+ }
211
+
212
+ function hideHover() {
213
+ if (hoverBox) hoverBox.hidden = true;
214
+ if (hoverLabel) hoverLabel.hidden = true;
215
+ }
216
+
217
+ function updateSelectOverlay() {
218
+ if (selectionLocked && selectedEl) {
219
+ placeBox(selectBox, selectLabel, selectedEl);
220
+ postSelectionRect();
221
+ }
222
+ }
223
+
224
+ function post(type, payload) {
225
+ window.parent.postMessage(
226
+ { source: 'design-bridge', type: type, payload: payload || {} },
227
+ '*',
228
+ );
229
+ }
230
+
231
+ function clearSelection(notifyParent) {
232
+ clearPreviewStyles();
233
+ selectedEl = null;
234
+ selectionLocked = false;
235
+ if (selectBox) selectBox.hidden = true;
236
+ if (selectLabel) selectLabel.hidden = true;
237
+ if (notifyParent) post('DESIGN_DESELECT');
238
+ }
239
+
240
+ function onMouseMove(e) {
241
+ if (!enabled) return;
242
+
243
+ if (selectionLocked && selectedEl) {
244
+ updateSelectOverlay();
245
+ hideHover();
246
+ return;
247
+ }
248
+
249
+ var el = pickElement(document.elementFromPoint(e.clientX, e.clientY));
250
+ if (!el) {
251
+ hideHover();
252
+ return;
253
+ }
254
+ placeBox(hoverBox, hoverLabel, el);
255
+ post('DESIGN_HOVER', elementPayload(el));
256
+ }
257
+
258
+ function onClick(e) {
259
+ if (!enabled) return;
260
+ e.preventDefault();
261
+ e.stopPropagation();
262
+
263
+ var el = pickElement(document.elementFromPoint(e.clientX, e.clientY));
264
+ if (!el) return;
265
+
266
+ if (selectionLocked && selectedEl) {
267
+ if (el === selectedEl) {
268
+ clearSelection(true);
269
+ return;
270
+ }
271
+ // Click fuera del popover / en otro nodo ya no deselecciona.
272
+ // clearSelection(true);
273
+ return;
274
+ }
275
+
276
+ selectedEl = el;
277
+ selectionLocked = true;
278
+ placeBox(selectBox, selectLabel, el);
279
+ hideHover();
280
+ post('DESIGN_SELECT', elementPayload(el));
281
+ }
282
+
283
+ function enable() {
284
+ if (enabled) return;
285
+ enabled = true;
286
+ ensureOverlay();
287
+ document.addEventListener('mousemove', onMouseMove, true);
288
+ document.addEventListener('click', onClick, true);
289
+ window.addEventListener('scroll', updateSelectOverlay, true);
290
+ window.addEventListener('resize', updateSelectOverlay);
291
+ document.body.style.cursor = 'crosshair';
292
+ post('DESIGN_READY');
293
+ }
294
+
295
+ function disable() {
296
+ if (!enabled) return;
297
+ enabled = false;
298
+ clearSelection(false);
299
+ document.removeEventListener('mousemove', onMouseMove, true);
300
+ document.removeEventListener('click', onClick, true);
301
+ window.removeEventListener('scroll', updateSelectOverlay, true);
302
+ window.removeEventListener('resize', updateSelectOverlay);
303
+ document.body.style.cursor = '';
304
+ hideHover();
305
+ }
306
+
307
+ window.addEventListener('message', function (event) {
308
+ var data = event.data;
309
+ if (!data || data.target !== 'design-bridge') return;
310
+ if (data.type === 'DESIGN_ENABLE') enable();
311
+ if (data.type === 'DESIGN_DISABLE') disable();
312
+ if (data.type === 'DESIGN_CLEAR_SELECTION') clearSelection(false);
313
+ if (data.type === 'DESIGN_APPLY_STYLES') applyPreviewStyles(data.payload && data.payload.styles);
314
+ if (data.type === 'DESIGN_CLEAR_PREVIEW') clearPreviewStyles();
315
+ });
316
+
317
+ post('DESIGN_READY');
318
+ })();
@@ -21,7 +21,28 @@ const postcssBrowserStub = fileURLToPath(
21
21
  )
22
22
 
23
23
  const frameworkRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
24
+ const designBridgePath = resolve(frameworkRoot, 'public', 'design-bridge.js')
24
25
 
26
+ /** Sirve /design-bridge.js desde el package (no desde public/ del usuario). */
27
+ function designBridgePlugin() {
28
+ return {
29
+ name: 'ecomiq:design-bridge',
30
+ configureServer(server) {
31
+ server.middlewares.use((req, res, next) => {
32
+ const url = req.url?.split('?')[0]
33
+ if (url !== '/design-bridge.js') return next()
34
+ try {
35
+ const body = readFileSync(designBridgePath)
36
+ res.setHeader('Content-Type', 'application/javascript; charset=utf-8')
37
+ res.setHeader('Cache-Control', 'no-store')
38
+ res.end(body)
39
+ } catch (error) {
40
+ next(error)
41
+ }
42
+ })
43
+ },
44
+ }
45
+ }
25
46
  function resolveSanitizeHtmlPostcss(source, importer) {
26
47
  if (source !== 'postcss') return null
27
48
  if (!importer?.includes('/sanitize-html/')) return null
@@ -160,7 +181,7 @@ export function createViteConfig({ appDir, config, projectRoot, runtime }) {
160
181
  const resolvedRuntime = resolveDevRuntime({ config, runtime })
161
182
  const resolvedHttps = resolveDevHttps({ config, projectRoot: root, appDir })
162
183
 
163
- const plugins = [tailwindcss()]
184
+ const plugins = [tailwindcss(), designBridgePlugin()]
164
185
 
165
186
  if (resolvedHttps.plugin) {
166
187
  plugins.push(resolvedHttps.plugin)
@@ -50,6 +50,10 @@ function RootDocument({ children, language }: { children: ReactNode; language: s
50
50
  </head>
51
51
  <body className="bg-background text-foreground antialiased">
52
52
  {children}
53
+ {/* Puente de design mode (solo dev): el parent (admin) pinta hover/select. */}
54
+ {import.meta.env.DEV ? (
55
+ <script src="/design-bridge.js?v=1.0.0" defer />
56
+ ) : null}
53
57
  <Scripts />
54
58
  </body>
55
59
  </html>