@ecomiq/storefront 2.0.18 → 2.0.20

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.20",
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",
@@ -44,12 +44,15 @@
44
44
  "./internal/thank-you": "./src/pages/thank-you.tsx",
45
45
  "./internal/cms-page": "./src/pages/pages.$pageSlug.tsx",
46
46
  "./internal/account": "./src/pages/account.tsx",
47
+ "./internal/api-proxy": "./src/pages/api.$.ts",
48
+ "./internal/auth-api": "./src/pages/api.auth.$.ts",
47
49
  "./package.json": "./package.json"
48
50
  },
49
51
  "files": [
50
52
  "bin",
51
53
  "src",
52
54
  "templates",
55
+ "public",
53
56
  "README.md"
54
57
  ],
55
58
  "sideEffects": [
@@ -79,7 +82,7 @@
79
82
  "vite": "^8.1.0",
80
83
  "wrangler": "^4.100.0",
81
84
  "zod": "^3.25.0",
82
- "@ecomiq/core": "^0.1.6"
85
+ "@ecomiq/core": "^0.1.7"
83
86
  },
84
87
  "peerDependencies": {
85
88
  "react": "^19.0.0",
@@ -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
+ })();
@@ -2,7 +2,6 @@ import { z } from "zod";
2
2
 
3
3
  export const customerLoginCustomerSchema = z.object({
4
4
  id: z.coerce.number(),
5
- publicId: z.union([z.coerce.number(), z.string()]),
6
5
  status: z.string(),
7
6
  isVerified: z.boolean(),
8
7
  });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Instancia Better Auth del storefront (servidor).
3
+ * Singleton usado por la ruta cerrada `/api/auth/$`.
4
+ */
5
+ import { createStorefrontAuth } from './create-storefront-auth'
6
+
7
+ export const storefrontAuth = createStorefrontAuth()
package/src/codegen.mjs CHANGED
@@ -17,6 +17,8 @@
17
17
  * /checkout/$checkoutId ← flujo de checkout
18
18
  * /thank-you ← post-compra
19
19
  * /account/* ← área de cuenta (login/register en template: /login, /register)
20
+ * /api/$ ← proxy BFF same-origin
21
+ * /api/auth/$ ← Better Auth (handlers del framework)
20
22
  */
21
23
  import {
22
24
  mkdirSync,
@@ -90,6 +92,20 @@ const FRAMEWORK_LOCKED_PAGES = [
90
92
  },
91
93
  ]
92
94
 
95
+ /** Server routes cerradas del framework (proxy BFF, Better Auth, etc.). */
96
+ const FRAMEWORK_LOCKED_API_ROUTES = [
97
+ {
98
+ rel: 'api/$.ts',
99
+ routeId: '/api/$',
100
+ importFrom: '@ecomiq/storefront/internal/api-proxy',
101
+ },
102
+ {
103
+ rel: 'api/auth.$.ts',
104
+ routeId: '/api/auth/$',
105
+ importFrom: '@ecomiq/storefront/internal/auth-api',
106
+ },
107
+ ]
108
+
93
109
  export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
94
110
  const pagesDir = resolve(cwd, 'pages')
95
111
  if (!existsSync(pagesDir)) {
@@ -209,11 +225,13 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
209
225
  }
210
226
 
211
227
  assertNoFrameworkPageOverrides(pageEntries)
228
+ assertNoFrameworkApiOverrides(apiEntries)
212
229
 
213
230
  const desiredRoutes = [
214
231
  ...pageEntries.map((entry) => entry.rel.replace(/\.jsx$/, '.tsx')),
215
232
  ...apiEntries.map((entry) => entry.rel),
216
233
  ...FRAMEWORK_LOCKED_PAGES.map((page) => page.rel),
234
+ ...FRAMEWORK_LOCKED_API_ROUTES.map((route) => route.rel),
217
235
  ].sort()
218
236
 
219
237
  for (const entry of pageEntries) {
@@ -244,6 +262,15 @@ export async function generateApp({ cwd, appDir, frameworkRoot, config }) {
244
262
  )
245
263
  }
246
264
 
265
+ for (const locked of FRAMEWORK_LOCKED_API_ROUTES) {
266
+ const outFile = join(routesDir, locked.rel)
267
+ mkdirSync(dirname(outFile), { recursive: true })
268
+ writeGeneratedFile(
269
+ outFile,
270
+ apiRouteFileSource(locked.routeId, locked.importFrom),
271
+ )
272
+ }
273
+
247
274
  reconcileGeneratedRoutes(appDir, routesDir, desiredRoutes)
248
275
 
249
276
  // ── 9. wrangler.jsonc para deploy en Cloudflare Workers ──
@@ -521,6 +548,23 @@ function assertNoFrameworkPageOverrides(pageEntries) {
521
548
  }
522
549
  }
523
550
 
551
+ function assertNoFrameworkApiOverrides(apiEntries) {
552
+ const reserved = new Map(
553
+ FRAMEWORK_LOCKED_API_ROUTES.map((route) => [route.routeId, route.rel]),
554
+ )
555
+
556
+ for (const entry of apiEntries) {
557
+ const routeId = fileToRouteId(entry.rel)
558
+ if (!reserved.has(routeId)) continue
559
+ throw new Error(
560
+ `La ruta ${routeId} es reservada por Ecomiq. ` +
561
+ `Elimina pages/${entry.rel}; ya la genera @ecomiq/storefront ` +
562
+ `(proxy BFF /api/$ y Better Auth /api/auth/$). ` +
563
+ `Puedes añadir otras rutas en pages/api/.`,
564
+ )
565
+ }
566
+ }
567
+
524
568
  /** Server route: reexporta GET/POST/... del módulo del usuario. */
525
569
  function apiRouteFileSource(routeId, importPath) {
526
570
  return (
package/src/index.ts CHANGED
@@ -31,6 +31,9 @@ export {
31
31
  DepartmentsSchema,
32
32
  DistrictSchema,
33
33
  DistrictsSchema,
34
+ EntityIdSchema,
35
+ NullableEntityIdSchema,
36
+ OptionalEntityIdSchema,
34
37
  GeoDivisionSchema,
35
38
  GeoDivisionTypeSchema,
36
39
  GetDepartmentsParamsSchema,
@@ -43,6 +46,7 @@ export {
43
46
  StorefrontMenuItemSchema,
44
47
  getMenuItems,
45
48
  } from '@ecomiq/core'
49
+ export type { EntityId } from '@ecomiq/core'
46
50
  export { getCountries, getDepartments, getDistricts, getProvinces } from '@ecomiq/core'
47
51
  export {
48
52
  AddCartItemParamsSchema,
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Proxy catch-all same-origin: `/api/*` → storefront `/v1/*`.
3
+ * Ruta cerrada del framework (no hace falta en pages/ del usuario).
4
+ */
5
+ import { proxyStorefrontRequest } from '../api-proxy'
6
+
7
+ type ApiHandlerArgs = {
8
+ request: Request
9
+ params: { _splat?: string }
10
+ }
11
+
12
+ async function handle({ request, params }: ApiHandlerArgs) {
13
+ return proxyStorefrontRequest(request, params._splat)
14
+ }
15
+
16
+ export const GET = handle
17
+ export const POST = handle
18
+ export const PUT = handle
19
+ export const PATCH = handle
20
+ export const DELETE = handle
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Better Auth catch-all: `/api/auth/*`.
3
+ * Ruta cerrada del framework (no hace falta en pages/ del usuario).
4
+ */
5
+ import { storefrontAuth } from '../auth/storefront-auth'
6
+
7
+ type ApiHandlerArgs = {
8
+ request: Request
9
+ }
10
+
11
+ async function handle({ request }: ApiHandlerArgs) {
12
+ return storefrontAuth.handler(request)
13
+ }
14
+
15
+ export const GET = handle
16
+ export const POST = handle
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * Auth storefront: Better Auth client + sesión via context.
3
3
  *
4
- * ```tsx
5
- * const client = createStorefrontAuthClient()
6
- * <AuthProvider client={client}>
7
- * <App />
8
- * </AuthProvider>
4
+ * El framework monta `<AuthProvider>` en `__root` (cliente por defecto).
5
+ * Puedes pasar un `client` propio si necesitas opciones custom.
9
6
  *
7
+ * ```tsx
10
8
  * const { session, signIn, signOut } = useAuth()
11
9
  * ```
12
10
  */
@@ -39,14 +37,29 @@ export type StorefrontSession = StorefrontAuthClientInstance['$Infer']['Session'
39
37
 
40
38
  const AuthContext = createContext<StorefrontAuthClientInstance | null>(null)
41
39
 
40
+ /** Cliente por defecto del framework (una instancia por bundle). */
41
+ let defaultAuthClient: StorefrontAuthClientInstance | null = null
42
+
43
+ function getDefaultAuthClient(): StorefrontAuthClientInstance {
44
+ if (!defaultAuthClient) {
45
+ defaultAuthClient = createStorefrontAuthClient()
46
+ }
47
+ return defaultAuthClient
48
+ }
49
+
42
50
  export function AuthProvider({
43
51
  client,
44
52
  children,
45
53
  }: {
46
- client: StorefrontAuthClientInstance
54
+ /** Si se omite, usa el cliente por defecto del storefront. */
55
+ client?: StorefrontAuthClientInstance
47
56
  children: ReactNode
48
57
  }) {
49
- return <AuthContext.Provider value={client}>{children}</AuthContext.Provider>
58
+ return (
59
+ <AuthContext.Provider value={client ?? getDefaultAuthClient()}>
60
+ {children}
61
+ </AuthContext.Provider>
62
+ )
50
63
  }
51
64
 
52
65
  /** Acceso a sesión y acciones de auth. Requiere `<AuthProvider>` + `<AppEcomiqProvider>`. */
@@ -17,14 +17,14 @@ import {
17
17
  useState,
18
18
  type ReactNode,
19
19
  } from 'react'
20
- import { type StorefrontCart } from '@ecomiq/core'
20
+ import { type StorefrontCart, EntityIdSchema } from '@ecomiq/core'
21
21
  import { z } from 'zod'
22
22
  import { serverApi } from '../server'
23
23
 
24
24
  const CartSessionSchema = z.object({
25
25
  version: z.literal(1),
26
26
  storeId: z.string().min(1),
27
- cartId: z.string().uuid(),
27
+ cartId: EntityIdSchema,
28
28
  })
29
29
 
30
30
  export type CartLoadStatus = 'loading' | 'ready' | 'error'
@@ -1 +1,2 @@
1
1
  export { AppEcomiqContext, AppEcomiqProvider, type AppEcomiqValue } from '../providers/ecomiq'
2
+ export { AuthProvider } from '../providers/auth'
package/src/server.ts CHANGED
@@ -58,6 +58,8 @@ import {
58
58
  type RemoveCartItemParams,
59
59
  type UpdateCartItemQuantityParams,
60
60
  type UploadPaymentReceiptParams,
61
+ EntityIdSchema,
62
+ OptionalEntityIdSchema,
61
63
  } from '@ecomiq/core'
62
64
  import { z } from 'zod'
63
65
  import { getStorefrontClient } from './client'
@@ -133,7 +135,7 @@ const removeCartItemFn = createServerFn({ method: 'POST' })
133
135
  .handler(({ data }) => submitCartItemRemoval(getStorefrontClient(), data))
134
136
 
135
137
  const AddProductToCartServerSchema = z.object({
136
- cartId: z.string().uuid().optional(),
138
+ cartId: OptionalEntityIdSchema,
137
139
  currencyCode: z.string().length(3),
138
140
  variantId: z.string().regex(/^[1-9]\d*$/),
139
141
  quantity: z.number().int().positive(),
@@ -172,7 +174,7 @@ const checkoutCartFn = createServerFn({ method: 'POST' })
172
174
  .handler(({ data }) => getStorefrontClient().checkoutCart(data))
173
175
 
174
176
  const CartPaymentReceiptServerSchema = z.object({
175
- cartId: z.string().uuid(),
177
+ cartId: EntityIdSchema,
176
178
  fileBase64: z.string().min(1),
177
179
  fileName: z.string().min(1),
178
180
  mimeType: z.string().min(1).optional(),
@@ -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)
@@ -2,7 +2,7 @@
2
2
  import { useState, type ReactNode } from 'react'
3
3
  import { createRootRoute, HeadContent, Scripts, Outlet, Link } from '@tanstack/react-router'
4
4
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
5
- import { AppEcomiqProvider } from '@ecomiq/storefront/internal/context'
5
+ import { AppEcomiqProvider, AuthProvider } from '@ecomiq/storefront/internal/context'
6
6
  import { buildRootHead } from '@ecomiq/storefront/internal/head'
7
7
  import { serverApi } from '@ecomiq/storefront'
8
8
  import { StorefrontError } from '@ecomiq/storefront/ui'
@@ -33,9 +33,11 @@ function RootComponent() {
33
33
  <RootDocument language={settings.store.language}>
34
34
  <QueryClientProvider client={queryClient}>
35
35
  <AppEcomiqProvider config={publicConfig} settings={settings}>
36
- <Layout config={publicConfig} settings={settings}>
37
- <Outlet />
38
- </Layout>
36
+ <AuthProvider>
37
+ <Layout config={publicConfig} settings={settings}>
38
+ <Outlet />
39
+ </Layout>
40
+ </AuthProvider>
39
41
  </AppEcomiqProvider>
40
42
  </QueryClientProvider>
41
43
  </RootDocument>
@@ -50,6 +52,10 @@ function RootDocument({ children, language }: { children: ReactNode; language: s
50
52
  </head>
51
53
  <body className="bg-background text-foreground antialiased">
52
54
  {children}
55
+ {/* Puente de design mode (solo dev): el parent (admin) pinta hover/select. */}
56
+ {import.meta.env.DEV ? (
57
+ <script src="/design-bridge.js?v=1.0.0" defer />
58
+ ) : null}
53
59
  <Scripts />
54
60
  </body>
55
61
  </html>